Repository: cotejp/webmidi Branch: master Commit: 9ef427c60f7e Files: 176 Total size: 1.8 MB Directory structure: gitextract_ohmewh1n/ ├── .babelrc ├── .editorconfig ├── .env.sample ├── .eslintrc.cjs ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── ask-a-question.md │ │ ├── report-a-bug.md │ │ └── suggest-a-new-feature.md │ └── workflows/ │ └── codeql-analysis.yml ├── .gitignore ├── .nycrc ├── BANNER.txt ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── examples/ │ ├── README.md │ ├── electron/ │ │ ├── README.md │ │ └── basic-example/ │ │ ├── README.md │ │ ├── index.html │ │ ├── main.js │ │ ├── package.json │ │ ├── preload.js │ │ └── renderer.js │ ├── next.js/ │ │ ├── README.md │ │ └── basic-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ └── pages/ │ │ └── index.js │ ├── p5.js/ │ │ ├── README.md │ │ ├── basic-example/ │ │ │ ├── index.html │ │ │ ├── sketch.js │ │ │ └── styles.css │ │ └── querying-note-state/ │ │ ├── index.html │ │ ├── sketch.js │ │ └── styles.css │ ├── quick-start/ │ │ └── index.html │ ├── react/ │ │ ├── README.md │ │ └── basic-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ ├── index.html │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── src/ │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ └── typescript/ │ ├── README.md │ └── basic-nodejs-example/ │ ├── index.ts │ └── package.json ├── package.json ├── scripts/ │ ├── api-documentation/ │ │ ├── generate-html.js │ │ ├── generate-markdown.js │ │ └── templates/ │ │ ├── core/ │ │ │ ├── class.hbs │ │ │ ├── constructor.hbs │ │ │ ├── enumerations.hbs │ │ │ ├── events.hbs │ │ │ ├── methods.hbs │ │ │ └── properties.hbs │ │ └── helpers/ │ │ ├── ddata.js │ │ ├── djip-helpers.js │ │ └── state.js │ ├── library/ │ │ ├── build.js │ │ ├── rollup.config.cjs.js │ │ ├── rollup.config.esm.js │ │ └── rollup.config.iife.js │ ├── sponsors/ │ │ └── retrieve-sponsors.js │ ├── typescript-declarations/ │ │ ├── generate.js │ │ └── generateOLD.js │ └── website/ │ └── deploy.js ├── src/ │ ├── Enumerations.js │ ├── Forwarder.js │ ├── Input.js │ ├── InputChannel.js │ ├── Message.js │ ├── Note.js │ ├── Output.js │ ├── OutputChannel.js │ ├── Utilities.js │ └── WebMidi.js ├── test/ │ ├── Enumerations.test.js │ ├── Forwarder.test.js │ ├── Input.test.js │ ├── InputChannel.test.js │ ├── Message.test.js │ ├── Note.test.js │ ├── Output.test.js │ ├── OutputChannel.test.js │ ├── Utilities.test.js │ ├── WebMidi.test.js │ └── support/ │ ├── JZZ.js │ ├── Utils.cjs.js │ └── Utils.iife.js ├── typescript/ │ └── webmidi.d.ts └── website/ ├── .gitignore ├── README.md ├── api/ │ ├── classes/ │ │ ├── Enumerations.md │ │ ├── EventEmitter.md │ │ ├── Forwarder.md │ │ ├── Input.md │ │ ├── InputChannel.md │ │ ├── Listener.md │ │ ├── Message.md │ │ ├── Note.md │ │ ├── Output.md │ │ ├── OutputChannel.md │ │ ├── Utilities.md │ │ ├── WebMidi.md │ │ └── _category_.json │ └── index.md ├── babel.config.js ├── blog/ │ └── 2021-12-01/ │ └── version-3-has-been-released.md ├── docs/ │ ├── archives/ │ │ ├── _category_.json │ │ ├── v1.md │ │ └── v2.md │ ├── getting-started/ │ │ ├── _category_.json │ │ ├── basics.md │ │ ├── installation.md │ │ └── supported-environments.md │ ├── going-further/ │ │ ├── _category_.json │ │ ├── electron.md │ │ ├── forwarding.md │ │ ├── middle-c.md │ │ ├── performance.md │ │ ├── sysex.md │ │ └── typescript.md │ ├── index.md │ ├── migration/ │ │ ├── _category_.json │ │ └── migration.md │ └── roadmap/ │ ├── _category_.json │ ├── under-evaluation.md │ ├── v3.md │ └── v4.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src/ │ ├── components/ │ │ ├── Button.js │ │ ├── Button.module.css │ │ ├── Button.module.scss │ │ ├── Column.js │ │ ├── Column.module.css │ │ ├── Column.module.scss │ │ ├── HomepageFeatures.js │ │ ├── HomepageFeatures.module.css │ │ ├── InformationBar.js │ │ ├── InformationBar.module.css │ │ └── InformationBar.module.scss │ ├── css/ │ │ ├── custom.css │ │ ├── custom.scss │ │ ├── index.css │ │ └── index.scss │ ├── pages/ │ │ ├── about/ │ │ │ └── index.md │ │ ├── index.js │ │ ├── index.module.css │ │ ├── index.module.scss │ │ ├── research/ │ │ │ └── index.md │ │ ├── showcase/ │ │ │ └── index.md │ │ ├── sponsors/ │ │ │ └── index.md │ │ └── tester/ │ │ └── index.js │ └── theme/ │ ├── CodeBlock/ │ │ └── index.js │ ├── Footer/ │ │ ├── index.js │ │ ├── styles.module.css │ │ └── styles.module.scss │ └── Navbar/ │ ├── index.js │ └── styles.module.css └── static/ ├── .nojekyll ├── js/ │ └── newsletter-popup.js └── styles/ ├── default.css ├── default.scss └── jsdoc.css ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "targets": {"node": "current"} } ] ], "env": { "test": { "plugins": [ "istanbul" ] } } } ================================================ FILE: .editorconfig ================================================ # EditorConfig helps maintain consistent coding styles for multiple developers working on the same # project across various editors and IDEs: https://editorconfig.org root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 2 max_line_length = 100 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [COMMIT_EDITMSG] max_line_length = 0 ================================================ FILE: .env.sample ================================================ # This is an example file. To actually use it, you need to rename it ".env". The .env file is used # by the dotenv module to set environment variables needed during development. The .env file MUST # NEVER BE committed. # Token used by the `release-it` module to create automatic GitHub releases. GITHUB_TOKEN=XXXXX ================================================ FILE: .eslintrc.cjs ================================================ module.exports = { "env": { "amd": true, "browser": true, "mocha": true, "node": true, "es6": true }, "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "globals": { "Promise": "readonly", "WebMidi": "readonly", "chai": "readonly", "sinon": "readonly", "expect": "readonly", "Note": "readonly", "isNative": "readonly", "config": "readonly" }, "extends": [ "eslint:recommended", "prettier", "plugin:react/recommended" ], // The idea here is to stick to the rules defined by Prettier (https://prettier.io/) and only make // exceptions in ESLint when absolutely necessary. "rules": { // Rules to align ESLint with Prettier (even though we are already using eslint-config-prettier) "indent": ["error", 2], "semi": ["error", "always"], "quote-props": ["error", "as-needed"], "quotes": ["error", "double", {"avoidEscape": true, "allowTemplateLiterals": true}], // Rules that knowingly change the default Prettier behaviour "no-multi-spaces": ["error", { "ignoreEOLComments": true }], "linebreak-style": ["error", "unix"], // Force \n instead of Prettier's auto-detect behaviour "no-trailing-spaces": ["error", { "skipBlankLines": true, "ignoreComments": true }], "max-len": ["error", { "code": 100, "comments": 150 }], // Prettier's 80 is too small. Period. "no-console": ["error", { "allow": ["info", "warn", "error"] }], // Only some (unlike Prettier) // Other rules "no-prototype-builtins": "off", "react/prop-types": "off" }, "settings": { "react": { "version": "detect" } } }; ================================================ FILE: .github/FUNDING.yml ================================================ # Up to 4 GitHub sponsors-enabled usernames e.g. [user1, user2] github: [djipco] # Single Patreon username #patreon: # Replace with a single Open Collective username #open_collective: # Replace with a single Ko-fi username #ko_fi: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel #tidelift: # Replace with a single Community Bridge project-name e.g., cloud-foundry #community_bridge: # Replace with a single Liberapay username #liberapay: # Replace with a single IssueHunt username #issuehunt: # Replace with a single Otechie username #otechie: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] #custom: ================================================ FILE: .github/ISSUE_TEMPLATE/ask-a-question.md ================================================ --- name: Ask a question about: This is to ask a usage, support or general question title: '' labels: '' assignees: '' --- **Questions should be submitted to the Forum** All questions should be submitted to the **Questions & Support** section of the WebMidi.js Forum: https://webmidijs.org/forum/ This opens up the question to the community while reserving GitHub strictly for bugs and issues. Thank you. ================================================ FILE: .github/ISSUE_TEMPLATE/report-a-bug.md ================================================ --- name: Report a bug about: This is only to report a bug, issue or problem. title: '' labels: '' assignees: '' --- **Description** Describe the problem and how to reproduce it. If appropriate, include code samples, screenshots, error messages, etc. **Environment:** Specify the environment where you are witnessing the problem: - Library version and flavour (CJS, ESM or IIFE) - Runtime (browser or Node.js) and version - Language (JavaScript or TypeScript) - Operating system **Details** Add any other information, context or details that could help track down the problem. ================================================ FILE: .github/ISSUE_TEMPLATE/suggest-a-new-feature.md ================================================ --- name: Suggest a new feature about: This is to suggest a new feature or improvement title: '' labels: '' assignees: '' --- **Enhancement proposals should be submitted in the Forum** To submit a new feature or improvement request, please post it to the **Enhancement Proposals** section of the WebMidi.js Forum: https://webmidijs.org/forum/ This allows the feature request to be discussed with the community while reserving GitHub strictly for bugs and issues. Thank you. ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '32 5 * * 4' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .gitignore ================================================ ### System ######################################################################################### .DS_Store* Icon? ._* Thumbs.db ehthumbs.db Desktop.ini .directory *~ *.tgz ### Editors ######################################################################################## .idea .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.sublime-project *.sublime-workspace ### Dependency Directories ######################################################################### node_modules ### Logs ########################################################################################### *.log ### Circle CI ###################################################################################### .circleci ### Test coverage ################################################################################## .nyc_output coverage ### Passwords, tokens and such ##################################################################### .credentials .env* !.env.sample ### Production build ############################################################################### dist ================================================ FILE: .nycrc ================================================ { "report-dir": "./node_modules/nyc/.nyc_output", "temp-dir": "./node_modules/nyc/.coverage" } ================================================ FILE: BANNER.txt ================================================ <%= pkg.webmidi.name %> v<%= pkg.version %> <%= pkg.webmidi.tagline %> <%= pkg.homepage %> Build generated on <%= moment().format('MMMM Do, YYYY') %>. © Copyright 2015-<%= moment().format('YYYY') %>, Jean-Philippe Côté. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: CHANGELOG.md ================================================ # Changelog Starting with version 3.x, all notable changes to WebMidi.js will be documented in this file. The format used is the one suggested by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [3.0.0] ### Added - WebMidi.js now has builtin **Node.js** support thanks to the [jzz](https://www.npmjs.com/package/jzz) node module by Jazz-Soft. - WebMidi.js is now available in IIFE (Immediately Invoked Function Expression), CJS (CommonJS) and ESM (ECMAScript module) flavours (all with normal and minifed versions with sourcemaps). - WebMidi.js now publishes a **TypeScript** definition file for CJS and ESM with all releases. - WebMidi.js now explicitly checks for Jazz-Plugin support in environments with no native support for the Web MIDI API. - The `WebMidi.enable()` method now returns a promise. The promise is fulfilled with the `WebMidi` object. It still supports using a callback. - `Input` and `Output` objects now emit `opened`, `closed` and `disconnected` events. - Hundreds of unit tests have been added and test coverage is now available. - There are new `InputChannel` and `OutputChannel` objects. They are used to communication with a single channel of an input or output. - All emitted events now have a `target` property referencing the object that triggered the event. - The `sendNoteOn()` method has been added. It behaves the same way as `playNote()` does except it does not accept a `duration` option. It was added mostly for completeness' sake. - The `sendNoteOff()` method has been added. It behaves the same way as `stopNote()` does. Actually, `stopNote()` is an alias to `sendNoteOff()`. - All methods of the `Output` object that communication with a device are prefixed with "send". This makes it easier to find the right method. Old method names are still usable but are deprecated. - The `sendChannelAftertouch()`, `sendKeyAftertouch()` and `sendPitchbend()` method now have a `useRawValue` options allowing the assignment of value using an integer between 0 and 127 instead of a float between 0 and 1. - There is a new `Note` object that can be used in various places such as when calling `playNote()` or `stopNote()`. It carries with it the note number, the duration (if any), the attack and release information, etc. - A `WebMidi.validation` property (defaults to `true`) can be used to disable all argument checking and legacy support throughout the library (for performance). This property can also be in the options of `WebMidi.enable()`. - The `send()` and `sendSysex()` methods of `Output` and `OutputChannel` can now officially use `Uint8Array` input (not supported on Node.js however). - Licence has been changed to Apache 2.0 - An `octaveOffset` property has been added to `Input`, `InputChannel`, `Output` and `OutputChannel`. This means you can offset the octave globally, at the input/output level or at the channel level - A `Message` object has been added. This allows easier routing of messages. - Added support for RPN messages and improved NRPN parsing. - A two-position array can now be passed to `sendControlChange()` to specify both MSB and LSB at once. - The `InputChannel` object offers a `getNoteState()` method that reports if a note is currently playing or not. It also has a new `notesState` property which is an array holding the playing status of all notes (0-127). - It is now possible to add a forwarder to an `Input` that will forward MIDI messages to a specified output. Also, the inbound messages can be filtered for forwarding by message type and channel. A new `Forwarder` class has been added for that purpose. - Added `WebMidi.version` - The `WedMidi` object now has a `defaults` property where you can set system-wide defaults such as the default `attack` and `release` velocity. More defaults to come! ### Changed - [BREAKING CHANGE] Passing `undefined` as the `channel` value to `addListener()` no longer means that all channels should be listening. This was a terrible design decision and it ends with version 3. - Documentation is now generated with [jsdoc](https://www.npmjs.com/package/jsdoc) instead of the outdated [yuidoc](https://www.npmjs.com/package/grunt-contrib-yuidoc). - [BREAKING CHANGE] The `"controlchange"` event's `value` property is now a float between 0 and 1. Its `rawValue` property now contains the 7bit integer value (between 0 and 127). - [BREAKING CHANGE] The `"nrpn"` event's `value` property is now a float between 0 and 1. Its `rawValue` property now contains the 16bit integer value (between 0 and 65535). - Grunt has been replaced with NPM scripts for all build purposes. - [BREAKING CHANGE] The `nrpnEventsEnabled` property has been moved from the `Input` class to the `InputChannel` class. Trying to access it will trigger a warning in the console. - [BREAKING CHANGE] The `getCcNameByNumber()` method has been moved from the `Input` class to the `InputChannel` class and now returns `undefined` instead of `false` when no matching name is found. - [BREAKING CHANGE] The `"tuningrequest"` event has been renamed `"tunerequest"`. - The event received by listeners registered on `Input` and `InputChannel` objects has been slightly changed. Its `data` property now contains a regular array (instead of a `Uint8Array`). Its `rawData` property now contains the `Uint8Array`. - Several methods have been moved from the `WebMidi` object to the `Utilities` object. Using the old methods will continue to work but will trigger a deprecation warning in the console. - The `send()` method now accepts a `Message` object. - If a device is disconnected and connected back, it will retain its state (such as listeners, etc.). This is particularly useful when the computer goes to sleep and is brought back online. - Several conversion methods have been added to the new `Utilities` class such as `from7bitToFloat()`, `fromFloatTo7Bit()`, `fromMsbLsbToFloat()`, `fromFloatToMsbLsb()`, etc. - All enumerations have been move to the `Enumerations` object (e.g. `MIDI_CHANNEL_MESSAGES`, `MIDI_CHANNEL_NUMBERS`, etc.) ### Deprecated - The `velocity` option parameter has been renamed `attack`. There are new `rawAttack` and `rawRelease` parameters that should be used instead of setting `rawVelocity` to `true`. - The `WebMidi.noteNameToNumber()` method was renamed and moved to `Utilities.toNoteNumber()`. The old method has been deprecated but will continue to work in v3.x. - The `WebMidi.toMIDIChannels()` method was renamed and moved to `Utilities.sanitizeChannels()`. The old method has been deprecated but will continue to work in v3.x. - The name of the `Output.sendTuningRequest()` method was changed to `Output.sendTuneRequest()`. The old name has been deprecated but will continue to work in v3.x. - The `on()` method of the `Input` class has been deprecated. Use `addListener()` instead. - The `InputChannel.nrpnEventsEnabled` property has been renamed to `InputChannel.parameterNumberEventsEnabled`. The old property is deprecated but will be kept for backwards compatibility. ### Removed - Support for Bower. ## [2.5.1] - 2019-08-25 Versions 2.5.x and earlier have not been tracked in this changelog. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at info@webmidijs.org. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing First off, **thank you** for considering to contribute to this project. You should know that there are many ways to contribute. You can write tutorials or blog posts, improve the documentation, submit bug reports or feature requests and write actual source code. All of these are very worthwhile contributions. Following these guidelines helps to communicate that you respect the time of the developers managing this open source project. In return, they will reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. ## Submitting a Feature Requests If you find yourself wishing for a feature, you are probably not alone. There are bound to be others out there with similar needs. Before submitting a feature request, first check if the [wiki](https://github.com/djipco/webmidi/wiki)'s enhancements section already lists that feature. If not, open an [issue](https://github.com/djipco/webmidi/issues) which describes the feature you would like to see, why you need it, and how it should work. ## Understanding How to Contribute Contribution to this project is done via pull requests. This allows the owner and contributors to properly review what gets merged into the project. **If this is your first pull request**, you can learn how to get started from a free series of tutorials called [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). ## Submitting a Pull Request **WebMidi.js** is a relatively small library supported by an even smaller team. Therefore, the process for contributing is intended to be simple and friendly. However, to insure good quality, there are steps that you should go through when submitting a PR. Here are the usual steps: 1. Discuss the change(s) you wish to make by means of an [issue](https://github.com/djipco/webmidi/issues). 2. Unless the PR is for a minor improvement (typo, documentation, etc.), you should write and/or update unit tests and check your code against the tests (see below). 3. If appropriate, update the [jsdoc](http://usejsdoc.org/) comments. Keeping the documentation and the API consistant is very important. 4. If appropriate, update the `README.md` file. Please note that your code should adhere to the styles defined in `.eslintrc.js`. You can use `npm run lint` to make sure it does. Finally, **do not** update the library's version number. Version numbering and releases will be handled by the owner. If the PR breaks backwards-compatibility, it must be communicated explicitely to the owner. The versioning scheme follows the [SemVer](http://semver.org/) standard. ## Testing WebMidi.js now has a proper test suite. The tests can be run on the command line without a need for a browser (thanks to Tim Susa). You can execute all tests, including code coverage, by running the following command in the terminal or on the command line: ``` npm run test-all ``` You can develop in *watch mode* with hot file reloading like so: ``` npm run test -- -w ``` You can start a single test in this way: ``` npx mocha ./test/virtual-midi-test.js ``` You can develop a single test in *watch mode* like this: ``` npx mocha ./test/virtual-midi-test.js -- -w ``` If you simply want to view code coverage, you can do: ``` npm run test-coverage ``` ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ================================================ FILE: README.md ================================================ ![WebMidi.js Logo](https://webmidijs.org/img/webmidijs-logo-color-on-white.svg "WebMidi.js") [![](https://data.jsdelivr.com/v1/package/npm/webmidi/badge)](https://www.jsdelivr.com/package/npm/webmidi) [![npm](https://img.shields.io/npm/dm/webmidi)](https://www.npmjs.com/package/webmidi) [![npm](https://img.shields.io/npm/dt/webmidi)](https://www.npmjs.com/package/webmidi) [![](https://img.shields.io/github/stars/djipco/webmidi?style=social)](https://github.com/djipco/webmidi) [![npm](https://img.shields.io/npm/l/webmidi)](https://www.npmjs.com/package/webmidi) ## Introduction **WEBMIDI.js** makes it easy to interact with MIDI instruments directly from a web browser or from Node.js. It simplifies the control of physical or virtual MIDI instruments with user-friendly functions such as `playNote()`, `sendPitchBend()` or `sendControlChange()`. It also allows reacting to inbound MIDI messages by adding listeners for events such as `"noteon"`, `"pitchbend"` or `"programchange"`. In short, the goal behind WEBMIDI.js is to get you started with your web-based MIDI project as quickly and efficiently as possible. ## Getting Started The [**official website**](https://webmidijs.org) site is the best place to get started. Over there, you will find, amongst others, two key resources: * [Documentation](https://webmidijs.org/docs/) * [API Reference](https://webmidijs.org/api/) To exchange with fellow users and myself, you can visit our [**Forum**](https://github.com/djipco/webmidi/discussions) which is hosted on the GitHub Discussions platform: * [Forum](https://github.com/djipco/webmidi/discussions) If you want to stay up-to-date, here are your best sources: * [Newsletter](https://mailchi.mp/eeffe50651bd/webmidijs-newsletter) * [Twitter](https://twitter.com/webmidijs) ## Sponsors WEBMIDI.js is a passion project but it still takes quite a bit of time, effort and money to develop and maintain. That's why I would like to sincerely thank 👏 these sponsors for their support: [](https://github.com/awatterott "@awatterott")   [](https://github.com/rubendax "@rubendax")   Anonymous Sponsor   [](https://github.com/philmillman "@philmillman")   Anonymous Sponsor   Anonymous Sponsor If you use the library and find it useful, please 💜 [**sponsor the project**](https://github.com/sponsors/djipco). ## Feature Request If you would like to request a new feature, enhancement or API change, please first check that it is not [already planned](https://webmidijs.org/docs/future-versions/next). Then, discuss it in the [Enhancement Proposals](https://github.com/djipco/webmidi/discussions/categories/feature-requests) section of the forum. ## Citing this Software in Research If you use this software for research or academic purposes, please cite the project in your references (or wherever appropriate). Here's an example of how to cite it ([APA Style](https://apastyle.apa.org/)): >Côté, J. P. (2021). WebMidi.js v3.0.0 [Computer Software]. Retrieved from https://github.com/djipco/webmidi Cheers! -- Jean-Philippe ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Support | Notes | | ------------- | ------------------ | ------------------------------------------------------------- | | 3.0.x (alpha) | :white_check_mark: | This is the current, officially-supported version. | | 2.5.x | :white_check_mark: | Bug and security fixes only. | | 1.0.x | :x: | This version has reached end of life. | ## Reporting a Vulnerability To report a security-related problem, **you should not file an issue on GitHub** as this might put users at risk. **Instead, send an email to jp@cote.cc**. ================================================ FILE: examples/README.md ================================================ # WEBMIDI.js Usage Examples In this directory, you will find starter examples to help you use WEBMIDI.js in various contexts or for various purposes. ## Browser Quick Start Example * [Quick Start](quick-start/) ## Frameworks * [Next.js](next.js/) * [p5.js](p5.js/) * [React](react/) ## Environments * [Electron](electron/) ## Languages * [TypeScript](typescript/) ## More Examples wanted! To submit a new example, simply send a pull request and it will be quickly reviewed. Please create a README.md file to provide information regarding how the example should be used. ================================================ FILE: examples/electron/README.md ================================================ # Using WEBMIDI.js with Electron A collection of examples to use WEBMIDI.js inside an Electron application. ## Examples * [**Basic Electron Example**](basic-example) ## Platform specific notes The permission requests must be properly handled in the main process before initializing WEBMIDI: ```javascript mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback, details) => { if (permission === 'midi' || permission === 'midiSysex') { callback(true); } else { callback(false); } }) mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin) => { if (permission === 'midi' || permission === 'midiSysex') { return true; } return false; }); ``` ================================================ FILE: examples/electron/basic-example/README.md ================================================ # Starter Template for Electron This is a minimal Electron application based on the [Quick Start Guide](https://electronjs.org/docs/latest/tutorial/quick-start) within the Electron documentation. ## To Use ```bash # Install dependencies npm install # Run the app npm start ``` ================================================ FILE: examples/electron/basic-example/index.html ================================================ WEBMIDI.js + Electron Demo

WEBMIDI.js + Electron Demo

Node.js , Chromium , Electron

WEBMIDI.js

MIDI Inputs:

MIDI Outputs:

================================================ FILE: examples/electron/basic-example/main.js ================================================ // Modules to control application life and create native browser window const {app, BrowserWindow} = require("electron"); const path = require("path"); function createWindow () { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "preload.js") } }); // Respond affirmatively to requests for MIDI and MIDI SysEx permissions mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback, details) => { if (permission === 'midi' || permission === 'midiSysex') { callback(true); } else { callback(false); } }) // Respond affirmatively to checks for MIDI and MIDI SysEx permissions mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin) => { if (permission === 'midi' || permission === 'midiSysex') { return true; } return false; }); // and load the index.html of the app. mainWindow.loadFile("index.html"); // Open dev tools // mainWindow.webContents.openDevTools(); } // This method will be called when Electron has finished initialization and is ready to create // browser windows. Some APIs can only be used after this event occurs. app.whenReady().then(() => { createWindow(); app.on("activate", function () { // On macOS it's common to re-create a window in the app when the dock icon is clicked and there // are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); // Quit when all windows are closed, except on macOS. There, it's common for applications and their // menu bar to stay active until the user quits explicitly with Cmd + Q. app.on("window-all-closed", function () { if (process.platform !== "darwin") app.quit(); }); // In this file you can include the rest of your app's specific main process code. You can also put // them in separate files and require them here. ================================================ FILE: examples/electron/basic-example/package.json ================================================ { "name": "webmidijs-electron-demo", "version": "1.0.0", "description": "A minimal Electron application", "main": "main.js", "scripts": { "start": "electron ." }, "license": "CC0-1.0", "devDependencies": { "electron": "^39.8.5" }, "dependencies": { "webmidi": "latest" } } ================================================ FILE: examples/electron/basic-example/preload.js ================================================ // All of the Node.js APIs are available in the preload process. It has the same sandbox as a Chrome // extension. window.addEventListener("DOMContentLoaded", () => { const replaceText = (selector, text) => { const element = document.getElementById(selector); if (element) element.innerText = text; }; for (const type of ["chrome", "node", "electron"]) { replaceText(`${type}-version`, process.versions[type]); } }); ================================================ FILE: examples/electron/basic-example/renderer.js ================================================ // This file is required by the index.html file and will be executed in the renderer process for // that window. No Node.js APIs are available in this process because `nodeIntegration` is turned // off. Use `preload.js` to selectively enable features needed in the rendering process. import {WebMidi} from "./node_modules/webmidi/dist/esm/webmidi.esm.js"; WebMidi.enable() .then(onEnabled) .catch(err => console.warning(err)); function onEnabled() { document.getElementById("webmidi-version").innerHTML += `v${WebMidi.version}`; WebMidi.inputs.forEach(input => { document.getElementById("webmidi-inputs").innerHTML += `${input.name}, `; }); WebMidi.outputs.forEach(output => { document.getElementById("webmidi-outputs").innerHTML += `${output.name}, `; }); } ================================================ FILE: examples/next.js/README.md ================================================ # Using WEBMIDI.js with Next.js A collection of examples to use WEBMIDI.js with the Next.js framework. ## Examples * [**Basic Next.js Example**](basic-example) ================================================ FILE: examples/next.js/basic-example/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env.local .env.development.local .env.test.local .env.production.local ================================================ FILE: examples/next.js/basic-example/README.md ================================================ # Starter Template for Next.js This is a starter template for [Learn Next.js](https://nextjs.org/learn). ## To Use ```bash # Install dependencies npm install # Run the app npm run dev ``` ================================================ FILE: examples/next.js/basic-example/package.json ================================================ { "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "next": "^15.5.15", "react": "^18.2.0", "react-dom": "^18.2.0", "webmidi": "latest" } } ================================================ FILE: examples/next.js/basic-example/pages/index.js ================================================ import Head from "next/head"; import {WebMidi} from "webmidi"; // const {WebMidi} = require("webmidi"); WebMidi.enable() .then(() => { WebMidi.inputs.forEach(input => { console.info("Detected input: ", input.name); input.addListener("noteon", e => console.info(e.type, e.note.identifier)); input.addListener("noteoff", e => console.info(e.type, e.note.identifier)); }); }) .catch(err => console.error(err)); export default function Home() { return (
Create Next App

Welcome to Next.js!

Get started by editing pages/index.js

Documentation →

Find in-depth information about Next.js features and API.

Learn →

Learn about Next.js in an interactive course with quizzes!

Examples →

Discover and deploy boilerplate example Next.js projects.

Deploy →

Instantly deploy your Next.js site to a public URL with Vercel.

) } ================================================ FILE: examples/p5.js/README.md ================================================ # Using WEBMIDI.js with p5.js A collection of examples to use WEBMIDI.js inside the p5.js framework. ## Examples * [**Basic Example**](basic-example): drawing circles on canvas when **note on** MIDI event is received. * [**Querying note state**](querying-note-state): draw keyboard keys in color when MIDI keys presssed ================================================ FILE: examples/p5.js/basic-example/index.html ================================================ WebMidi.js + p5.js - Basic Example

WebMidi.js + p5.js - Basic Example

Randomly draw circles when a note on event is received on MIDI channel 1

================================================ FILE: examples/p5.js/basic-example/sketch.js ================================================ function setup() { createCanvas(window.innerWidth, window.innerHeight); noStroke(); // Enable WebMidi.js and trigger the onWebMidiEnabled() function when ready. WebMidi.enable() .then(onWebMidiEnabled) .catch(err => alert(err)); } function draw() { } function onWebMidiEnabled() { // Check if at least one MIDI input is detected. If not, display warning and quit. if (WebMidi.inputs.length < 1) { alert("No MIDI inputs detected."); return; } // Add a listener on all the MIDI inputs that are detected WebMidi.inputs.forEach(input => { // When a "note on" is received on MIDI channel 1, generate a random color start input.channels[1].addListener("noteon", function() { fill(random(255), random(255), random(255)); circle(random(width), random(height), 100); }); }); } ================================================ FILE: examples/p5.js/basic-example/styles.css ================================================ html, body { width: 100vw; height: 100vh; margin: 0; } main { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; } h1, p { margin: 0; padding: 16px 16px 0 16px; } ================================================ FILE: examples/p5.js/querying-note-state/index.html ================================================ WebMidi.js + p5.js - Querying Note State

WebMidi.js + p5.js - Querying Note State

Draw colored rectangles when specific notes are pressed.

================================================ FILE: examples/p5.js/querying-note-state/sketch.js ================================================ let channel; async function setup() { // Enable WebMidi.js await WebMidi.enable(); // Display available inputs in console (use the name to retrieve it) console.log(WebMidi.inputs); const input = WebMidi.getInputByName("MPK mini 3"); channel = input.channels[1]; // Create canvas createCanvas(500, 200); } function draw() { // Check if WebMidi is enabled and channel has been assigned before moving on if (!channel) return; // Draw blank keys for (let i = 0; i < 8; i++) { // Default fill is white fill("white"); // Each key has its own color. When it's pressed, we draw it in color (instead of white) if (i === 0 && channel.getNoteState("C4")) { fill("yellow"); } else if (i === 1 && channel.getNoteState("D4")) { fill("red"); } else if (i === 2 && channel.getNoteState("E4")) { fill("pink"); } else if (i === 3 && channel.getNoteState("F4")) { fill("orange"); } else if (i === 4 && channel.getNoteState("G4")) { fill("purple"); } else if (i === 5 && channel.getNoteState("A4")) { fill("green"); } else if (i === 6 && channel.getNoteState("B4")) { fill("turquoise"); } else if (i === 7 && channel.getNoteState("C5")) { fill("blue"); } // Draw the keys rect(85 + i * 40, 50, 30, 100); } } ================================================ FILE: examples/p5.js/querying-note-state/styles.css ================================================ html, body { width: 100vw; height: 100vh; margin: 0; } h1, p { margin: 0; padding: 16px 16px 0 16px; } ================================================ FILE: examples/quick-start/index.html ================================================ WebMidi.js Quick Start

WebMidi.js Quick Start

================================================ FILE: examples/react/README.md ================================================ # Using WEBMIDI.js with p5.js A collection of examples to use WEBMIDI.js inside the React framework. ## Examples * [**Basic Example**](basic-example) ================================================ FILE: examples/react/basic-example/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react/basic-example/README.md ================================================ # Getting Started with Create React App This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts In the project directory, you can run: ### `yarn start` Runs the app in the development mode.\ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. ### `yarn test` Launches the test runner in the interactive watch mode.\ See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `yarn build` Builds the app for production to the `build` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.\ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### `yarn eject` **Note: this is a one-way operation. Once you `eject`, you can’t go back!** If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). ### Code Splitting This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) ### Analyzing the Bundle Size This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) ### Making a Progressive Web App This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) ### Advanced Configuration This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) ### Deployment This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) ### `yarn build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) ================================================ FILE: examples/react/basic-example/package.json ================================================ { "name": "basic-example", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "^5.0.0", "web-vitals": "^2.1.2", "webmidi": "latest" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: examples/react/basic-example/public/index.html ================================================ React App
================================================ FILE: examples/react/basic-example/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: examples/react/basic-example/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: examples/react/basic-example/src/App.css ================================================ .App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: examples/react/basic-example/src/App.js ================================================ import logo from './logo.svg'; import './App.css'; import {WebMidi} from "webmidi"; function App() { WebMidi.enable() .then(() => console.log(WebMidi.inputs)) .catch(err => console.log(err)); return (
logo

Edit src/App.js and save to reload.

Learn React
); } export default App; ================================================ FILE: examples/react/basic-example/src/App.test.js ================================================ import { render, screen } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: examples/react/basic-example/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: examples/react/basic-example/src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( , document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: examples/react/basic-example/src/reportWebVitals.js ================================================ const reportWebVitals = onPerfEntry => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: examples/react/basic-example/src/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; ================================================ FILE: examples/typescript/README.md ================================================ # Using WEBMIDI.js in a TypeScript project Version 3 of WEBMIDI.js officially supports TypeScript. Type declarations are available for the ESM (ECMAScript modules) and CJS (CommonJS, a.k.a. Node.js modules) flavours in the `/dist` directory. To use the example, `cd` into the example directory, install the necessary modules with `npm install` and write some TypeScript code. You can generate the final JavaScript code with `tsc myFile.ts`. This should yield a `myFile.js` which you can run with `node myFile.js`. ## Examples * [**Basic Example**](basic-nodejs-example): listing input and output ports and reacting to pressed keyboard keys. ================================================ FILE: examples/typescript/basic-nodejs-example/index.ts ================================================ import {WebMidi} from "webmidi"; async function start() { await WebMidi.enable(); // List available inputs console.log("Available inputs: "); WebMidi.inputs.forEach(input => { console.log("\t" + input.name); input.addListener("noteon", e => console.log(e.note.identifier)); }); // List available outputs console.log("Available outputs: "); WebMidi.outputs.forEach(output => { console.log("\t" + output.name); }); } start(); ================================================ FILE: examples/typescript/basic-nodejs-example/package.json ================================================ { "name": "typescript-basic-nidejs-example", "version": "1.0.0", "main": "index.js", "devDependencies": { "typescript": "^4.5.4" }, "dependencies": { "webmidi": "latest" } } ================================================ FILE: package.json ================================================ { "name": "webmidi", "version": "3.1.16", "description": "WEBMIDI.js makes it easy to talk to MIDI instruments from a browser or from Node.js. It simplifies the control of external or virtual MIDI instruments with functions such as playNote(), sendPitchBend(), sendControlChange(), etc. It also allows reacting to incoming MIDI messages by adding listeners for user-friendly events such as 'noteon', 'pitchbend', 'songposition', etc.", "main": "./dist/cjs/webmidi.cjs.min.js", "types": "./dist/cjs/webmidi.cjs.d.ts", "exports": { ".": { "require": { "types": "./dist/cjs/webmidi.cjs.d.ts", "default": "./dist/cjs/webmidi.cjs.min.js" }, "import": { "types": "./dist/esm/webmidi.esm.d.ts", "default": "./dist/esm/webmidi.esm.js" } } }, "author": { "name": "Jean-Philippe Côté", "email": "webmidi@djip.co", "url": "https://github.com/djipco/" }, "repository": { "type": "git", "url": "https://github.com/djipco/webmidi.git" }, "bugs": { "url": "https://github.com/djipco/webmidi/issues" }, "files": [ "/dist" ], "keywords": [ "midi", "message", "web", "browser", "front-end", "web midi api", "music", "djipco", "music", "protocol", "communication", "channel", "node", "instrument", "device", "note" ], "homepage": "https://webmidijs.org", "license": "Apache-2.0", "webmidi": { "name": "WEBMIDI.js", "tagline": "A JavaScript library to kickstart your MIDI projects" }, "engines": { "node": ">=8.5" }, "scripts": { "library:build-all": "npm run lint && npm run library:build-cjs && npm run library:build-esm && npm run library:build-iife", "library:build-cjs": "node scripts/library/build.js -t cjs && npm run typescript-declaration:generate -- -t cjs", "library:build-esm": "node scripts/library/build.js -t esm && npm run typescript-declaration:generate -- -t esm", "library:build-iife": "node scripts/library/build.js -t iife", "test-coverage:report-as-text": "nyc --reporter=text mocha", "test-coverage:report-as-html": "nyc --reporter=html mocha", "website:build": "npm run --prefix website build", "website:serve": "npm run --prefix website serve", "website:dev-server": "npm run --prefix website start", "website:generate+push-to-gh-pages": "npm run jsdoc:generate-markdown+push && npm run --prefix website build && node scripts/website/deploy.js", "jsdoc:generate-html+push": "node scripts/api-documentation/generate-html.js", "jsdoc:generate-markdown+push": "node scripts/api-documentation/generate-markdown.js", "typescript-declaration:generate": "node scripts/typescript-declarations/generate.js", "lint": "eslint ./src/*.js", "release": "dotenv release-it --", "release:alpha": "npm run release -- prepatch --preRelease=alpha --npm.tag=next --github.preRelease", "sponsors:update": "node scripts/sponsors/retrieve-sponsors.js", "test:all": "mocha", "test:Input": "mocha test/Input.test.js", "test:InputChannel": "mocha test/InputChannel.test.js", "test:Note": "mocha test/Note.test.js", "test:Message": "mocha test/Message.test.js", "test:Enumerations": "mocha test/Enumerations.test.js", "test:Forwarder": "mocha test/Forwarder.test.js", "test:Output": "mocha test/Output.test.js", "test:OutputChannel": "mocha test/OutputChannel.test.js", "test:Utilities": "mocha test/Utilities.test.js", "test:WebMidi": "mocha test/WebMidi.test.js" }, "dependencies": { "djipevents": "^2.0.7" }, "devDependencies": { "@alexbinary/rimraf": "^1.0.2", "@babel/cli": "^7.13.0", "@babel/core": "^7.9.0", "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/polyfill": "^7.8.7", "@babel/preset-env": "^7.9.0", "@julusian/midi": "^3.1.0", "@octokit/graphql": "^4.8.0", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-replace": "^5.0.2", "array-back": "^6.2.0", "babel-plugin-istanbul": "^6.0.0", "chai": "^4.3.3", "docusaurus": "^1.14.7", "dotenv-cli": "^5.1.0", "eslint": "^7.32.0", "eslint-config-prettier": "^7.2.0", "eslint-plugin-react": "^7.26.1", "foodoc": "0.0.9", "fs-extra": "^10.0.0", "ftp-deploy": "^2.3.7", "gh-pages": "^5.0.0", "handlebars": "^4.7.9", "jsdoc": "^3.6.7", "jsdoc-to-markdown": "^7.1.0", "markdown-it": "^12.3.2", "marked": "^4.0.10", "minimist": "^1.2.5", "mocha": "^10.0.0", "moment": "^2.29.4", "nyc": "^15.0.1", "object-get": "^2.1.1", "prepend-file": "^2.0.0", "reduce-flatten": "^3.0.1", "release-it": "^19.0.4", "replace-in-file": "^6.3.2", "rollup": "^3.30.0", "rollup-plugin-license": "^2.3.0", "rollup-plugin-strip-code": "^0.2.7", "rollup-plugin-terser": "^5.3.0", "rollup-plugin-version-injector": "^1.3.3", "semver": "^7.3.5", "simple-git": "^3.32.3", "sinon": "^9.0.1", "sinon-browser-only": "^1.12.1", "system-commands": "^1.1.7", "test-value": "^3.0.0", "util": "^0.12.4" }, "release-it": { "git": { "commitMessage": "Release v${version}" }, "github": { "release": true, "draft": false, "releaseName": "Release v${version}", "assets": [ "webmidi-${version}.tgz" ] }, "hooks": { "after:bump": [ "npm run library:build-all" ], "before:git:release": [ "npm pack" ], "after:release": "rimraf webmidi-${version}.tgz" } }, "optionalDependencies": { "jzz": "^1.8.5" } } ================================================ FILE: scripts/api-documentation/generate-html.js ================================================ // This script generates web-based API documentation files from jsdoc comments in the source code // and commits them to the '/archives/api' directory of the 'gh-pages' branch. This makes them // available at djipco.githib.io/webmidi/archives/api/v3 // Modules const fs = require("fs-extra"); const fsPromises = require("fs").promises; const git = require("simple-git")(); const pkg = require("../../package.json"); const moment = require("moment"); const path = require("path"); const os = require("os"); const rimraf = require("@alexbinary/rimraf"); const system = require("system-commands"); // Some paths const CUSTOM_CSS = "../css/custom.css"; const TARGET_BRANCH = "gh-pages"; const TARGET_PATH = "./archives/api/v" + pkg.version.split(".")[0]; // Google Analytics configuration const GA_CONFIG = { ua: "UA-162785934-1", domain: "https://djipco.github.io/webmidi" }; // JSDoc configuration object to write as configuration file const config = { tags: { allowUnknownTags: true }, // The opts property is for command-line arguments passed directly in the config file opts: { template: "./node_modules/foodoc/template" }, // Source files source: { include: [ "./src/Enumerations.js", "./src/Forwarder.js", "./src/Input.js", "./src/InputChannel.js", "./src/Message.js", "./src/Note.js", "./src/Output.js", "./src/OutputChannel.js", "./src/Utilities.js", "./src/WebMidi.js", "./node_modules/djipevents/src/djipevents.js" ] }, sourceType: "module", plugins: [ "plugins/markdown" ], // Configurations for the Foodoc template templates: { systemName: `${pkg.webmidi.name} API`, systemSummary: pkg.webmidi.tagline, // systemLogo: LOGO_PATH, systemColor: "#ffcf09", copyright: `© ${pkg.author.name}, ` + `2015-${new Date().getFullYear()}. ` + `${pkg.webmidi.name} v${pkg.version} is released under the ${pkg.license} license.`, navMembers: [ {kind: "class", title: "Classes", summary: "All documented classes."}, {kind: "external", title: "Externals", summary: "All documented external members."}, // {kind: "global", title: "Globals", summary: "All documented globals."}, {kind: "mixin", title: "Mixins", summary: "All documented mixins."}, {kind: "interface", title: "Interfaces", summary: "All documented interfaces."}, {kind: "module", title: "Modules", summary: "All documented modules."}, {kind: "namespace", title: "Namespaces", summary: "All documented namespaces."}, {kind: "tutorial", title: "Tutorials", summary: "All available tutorials."} ], analytics: GA_CONFIG, stylesheets: [ CUSTOM_CSS ], dateFormat: "MMMM Do YYYY @ H:mm:ss", sort: "longname, linenum, version, since", collapseSymbols: true } }; function log(message) { console.info("\x1b[32m", message, "\x1b[0m"); } async function execute() { // Temporary target folder const TMP_SAVE_PATH = await fsPromises.mkdtemp(path.join(os.tmpdir(), "webmidi-api-doc-")); const CONF_PATH = TMP_SAVE_PATH + "/.jsdoc.json"; // Write temporary configuration file fs.writeFileSync(CONF_PATH, JSON.stringify(config)); // Prepare jsdoc command and generate documentation const cmd = "./node_modules/.bin/jsdoc " + `--configure ${CONF_PATH} ` + `--destination ${TMP_SAVE_PATH}`; await system(cmd); log(`Documentation temporarily generated in "${TMP_SAVE_PATH}"`); // Remove temporary configuration file await rimraf(CONF_PATH); // Here, we remove the index.html page and replace it with the list_class.html file await fs.copy( TMP_SAVE_PATH + "/list_class.html", TMP_SAVE_PATH + "/index.html", {overwrite: true} ); // Get current branch (so we can come back to it later) let results = await git.branch(); const ORIGINAL_BRANCH = results.current; // Switch to target branch log(`Switching from '${ORIGINAL_BRANCH}' branch to '${TARGET_BRANCH}' branch`); await git.checkout(TARGET_BRANCH); // Move dir to final destination and commit await fs.move(TMP_SAVE_PATH, TARGET_PATH, {overwrite: true}); await git.add([TARGET_PATH]); await git.commit("Updated on: " + moment().format(), [TARGET_PATH]); await git.push(); log(`Changes committed to ${TARGET_PATH} folder of '${TARGET_BRANCH}' branch`); // Come back to original branch log(`Switching back to '${ORIGINAL_BRANCH}' branch`); await git.checkout(ORIGINAL_BRANCH); } // Execute and catch errors if any (in red) execute().catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); ================================================ FILE: scripts/api-documentation/generate-markdown.js ================================================ // Imports const djipHelpers = require("./templates/helpers/djip-helpers.js"); const fs = require("fs-extra"); const git = require("simple-git")(); const Handlebars = require("handlebars"); const jsdoc2md = require("jsdoc-to-markdown"); const moment = require("moment"); const path = require("path"); const process = require("process"); // Paths const ROOT_PATH = process.cwd(); const SOURCE_DIR = path.resolve(ROOT_PATH, "src"); const TARGET_PATH = path.join(process.cwd(), "website", "api", "classes"); const TEMPLATE_DIR = path.resolve(ROOT_PATH, "scripts/api-documentation/templates/"); const DJIPEVENTS = path.resolve(ROOT_PATH, "node_modules/djipevents/src/djipevents.js"); // Register some Handlebars helpers Handlebars.registerHelper({ eventName: djipHelpers.eventName, stripNewlines: djipHelpers.stripNewlines, inlineLinks: djipHelpers.inlineLinks, methodSignature: djipHelpers.methodSignature, eq: djipHelpers.eq, ne: djipHelpers.ne, lt: djipHelpers.lt, gt: djipHelpers.gt, lte: djipHelpers.lte, gte: djipHelpers.gte, and: djipHelpers.and, or: djipHelpers.or, curly: djipHelpers.curly, createEventAnchor: djipHelpers.createEventAnchor, }); async function generate() { // Get source files list let files = await fs.readdir(SOURCE_DIR); files = files.map(file => path.resolve(SOURCE_DIR, file)); files.push(DJIPEVENTS); // Compute JSON from JSDoc const data = jsdoc2md.getTemplateDataSync({files: files}); // Build list of classes (explicitly adding Listener and EventEmitter) let classes = files.map(file => path.basename(file, ".js")).filter(file => file !== "djipevents"); classes.push("Listener", "EventEmitter"); // Parse each class file and save parsed output classes.forEach(filepath => { const basename = path.basename(filepath, ".js"); const filtered = data.filter(x => x.memberof === basename || x.id === basename); // Save markdown files fs.writeFileSync( path.resolve(TARGET_PATH, `${basename}.md`), parseFile(filtered) ); console.info(`Saved markdown file to ${basename}.md`); }); // Commit generated files await git.add([TARGET_PATH]); await git.commit("Automatically generated on: " + moment().format(), [TARGET_PATH]); console.info(`Files in ${TARGET_PATH} committed to git`); await git.push(); console.info(`Files pushed to remote`); } function parseFile(data) { let output = ""; let hbs; let filtered; // Sort elements according to kind and then according to name const order = {class: 0, constructor: 1, function: 2, member: 3, event: 4, enum: 5, typedef: 6}; data.sort((a, b) => { if (order[a.kind] === order[b.kind]) { return a.id.localeCompare(b.id); } else { return order[a.kind] < order[b.kind] ? -1 : 1; } }); // Sort 'fires' if present data.forEach(element => { if (Array.isArray(element.fires)) element.fires.sort(); }); // Class filtered = data.filter(el => el.kind === "class")[0]; hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/class.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Constructor filtered = data.filter(el => el.kind === "constructor")[0]; hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/constructor.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Members filtered = data.filter(el => el.kind === "member" && el.access !== "private"); hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/properties.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Methods filtered = data.filter(el => el.kind === "function" && el.access !== "private"); hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/methods.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Events filtered = data.filter(el => el.kind === "event" && el.access !== "private"); hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/events.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Enums filtered = data.filter(el => el.kind === "enum" && el.access !== "private"); hbs = fs.readFileSync(path.resolve(TEMPLATE_DIR, `core/enumerations.hbs`), {encoding: "utf-8"}); output += Handlebars.compile(hbs)(filtered); // Strip out links to Listener class // output = output.replaceAll("[**Listener**](Listener)", "`Listener`"); // output = output.replaceAll("[Listener](Listener)", "`Listener`"); // output = output.replaceAll("[**arguments**](Listener#arguments)", "`arguments`"); return output; } generate().catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); ================================================ FILE: scripts/api-documentation/templates/core/class.hbs ================================================ {{! This template creates the 'class' portion of the output (at the top). It relies on the following helpers: - inlineLinks - eq - eventName }} {{! Class name }} # {{name}} {{! Class description }} {{{inlineLinks description}}} {{! Since }} {{#if since}} **Since**: {{since}} {{/if}} {{! Extends }} {{#if augments}} **Extends**: {{#each augments}}[`{{this}}`]({{this}}){{#unless @last}}, {{/unless}}{{/each}} {{/if}} {{! Fires }} {{#if fires}} **Fires**: {{#each fires}}[`{{eventName this}}`](#event:{{eventName this}}){{#unless @last}}, {{/unless}}{{/each}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/core/constructor.hbs ================================================ {{! This template creates the 'constructor' portion of the output (below the class). It relies on the following helpers: - stripNewlines }} {{#if this}} {{! Constructor name }} ### `Constructor` {{! Description }} {{{inlineLinks description}}} {{! Parameters }} {{#if params}} **Parameters** > `new {{this.longname}}({{methodSignature this}})`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | {{#each params}} |{{#if this.optional}}[{{/if}}**`{{this.name}}`**{{#if this.optional}}]{{/if}} | {{#each this.type.names}}{{this}}
{{/each}} |{{this.defaultvalue}}|{{{stripNewlines (inlineLinks this.description)}}}| {{/each}}
{{/if}} {{! Exceptions }} {{#if this.exceptions}} **Throws**: {{#each this.exceptions}} * {{#if this.type}}`{{this.type.names.[0]}}` : {{/if}}{{inlineLinks this.description}} {{/each}} {{/if}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/core/enumerations.hbs ================================================ {{! This template creates the 'enums' portion of the output . It relies on the following helpers: - inlineLinks - stripNewlines }} {{#if this}} *** ## Enums {{#each this}} {{! Name }} ### `.{{this.name}}` {{curly true}}#{{this.name}}{{curly}} **Type**: {{this.type.names.[0]}}
{{! Attributes }} {{#if (eq this.scope "static")}} **Attributes**: static {{/if}} {{! Description }} {{{inlineLinks description}}} {{/each}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/core/events.hbs ================================================ {{! This template creates the 'Events' portion of the output . It relies on the following helpers: - inlineLinks - stripNewlines }} {{#if this}} *** ## Events {{#each this}} {{! Name and anchor }} ### `{{this.name}}` {{curly true}}{{createEventAnchor this.name}}{{curly}} {{! Description }} {{{inlineLinks description}}} {{! Since }} {{#if since}} **Since**: {{since}} {{/if}} {{! Properties }} {{#if properties}} **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | {{#each properties}} |**`{{this.name}}`** |{{this.type.names.[0]}}|{{{stripNewlines (inlineLinks this.description)}}}| {{/each}} {{/if}} {{/each}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/core/methods.hbs ================================================ {{! This template creates the 'Events' portion of the output . It relies on the following helpers: - inlineLinks - stripNewlines }} {{#if this}} *** ## Methods {{#each this}} {{! Name }} ### `.{{this.name}}({{#if params}}...{{/if}})` {{curly true}}#{{this.name}}{{curly}} {{! Since }} {{#if this.since}} **Since**: {{this.since}}
{{/if}} {{! Attributes }} {{#if (eq this.async true)}} **Attributes**: async {{/if}} {{! Description }} {{{inlineLinks description}}} {{! Parameters }} {{#if params}} **Parameters** > Signature: `{{this.name}}({{methodSignature this}})`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | {{#each params}} |{{#if this.optional}}[{{/if}}**`{{this.name}}`**{{#if this.optional}}]{{/if}} | {{#each this.type.names}}{{this}}
{{/each}} |{{this.defaultvalue}}|{{{stripNewlines (inlineLinks this.description)}}}| {{/each}}
{{/if}} {{! Returns }} {{#if returns}} **Return Value** {{! Return value description }} > Returns: {{#each returns~}} {{#if type~}} {{#each type.names~}} `{{{this}}}`{{#unless @last}} or {{/unless}} {{~/each}}
{{/if~}} {{~#if description}} {{{inlineLinks description}}} {{/if~}} {{~/each}} {{/if}} {{! Attributes }} {{#if (eq this.scope "static")}} **Attributes**: static {{/if}} {{#if this.exceptions}} **Throws**: {{#each this.exceptions}} * {{#if this.type}}`{{this.type.names.[0]}}` : {{/if}}{{{inlineLinks this.description}}} {{/each}} {{/if}} {{/each}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/core/properties.hbs ================================================ {{! This template creates the 'properties' portion of the output . It relies on the following helpers: - inlineLinks - stripNewlines }} {{#if this}} *** ## Properties {{#each this}} {{! Name }} ### `.{{this.name}}` {{curly true}}#{{this.name}}{{curly}} {{! Since }} {{#if this.since}} **Since**: {{this.since}}
{{/if}} {{! Type }} **Type**: {{this.type.names.[0]}}
{{#if (or this.readonly this.nullable)}} {{! Attributes }} **Attributes**: {{#if this.readonly}}read-only{{/if}}{{#if this.nullable}}, nullable{{/if}}{{#if (eq this.scope "static")}}, static{{/if}}
{{/if}} {{! Description }} {{{inlineLinks description}}} {{! Properties }} {{#if properties}} **Properties** | Property | Type | Description | | ------------ | ------------ | ------------ | {{#each properties}} |**`{{this.name}}`** |{{this.type.names.[0]}}|{{{stripNewlines (inlineLinks this.description)}}}| {{/each}} {{/if}} {{/each}} {{/if}} ================================================ FILE: scripts/api-documentation/templates/helpers/ddata.js ================================================ var arrayify = require('array-back'); var util = require('util'); var handlebars = require('handlebars'); var marked = require('marked'); var objectGet = require('object-get'); var where = require('test-value').where; var flatten = require('reduce-flatten'); var state = require('./state.js'); /** * ddata is a collection of handlebars helpers for working with the documentation data output by * [jsdoc-parse](https://github.com/75lb/jsdoc-parse). * * @module * @example * ```js * var handlebars = require("handlebars") * var ddata = require("ddata") * var docs = require("./docs.json") // jsdoc-parse output * * handlebars.registerHelper(ddata) * var template = * "{{#module name='yeah-module'}}\ * The author of the module is: {{author}}.\ * {{/module}}" * console.log(handlebars.compile(template)(docs)) * ``` */ /* utility block helpers */ exports.link = link; exports.returnSig2 = returnSig2; exports.sig = sig; exports.children = children; exports.indexChildren = indexChildren; /* helpers which return objects */ exports._link = _link; /* helpers which return booleans */ exports.isClass = isClass; exports.isClassMember = isClassMember; exports.isConstructor = isConstructor; exports.isFunction = isFunction; exports.isConstant = isConstant; exports.isEvent = isEvent; exports.isEnum = isEnum; exports.isTypedef = isTypedef; exports.isCallback = isCallback; exports.isModule = isModule; exports.isMixin = isMixin; exports.isExternal = isExternal; exports.isPrivate = isPrivate; exports.isProtected = isProtected; exports.showMainIndex = showMainIndex; /* helpers which return lists */ exports._orphans = _orphans; exports._identifiers = _identifiers; exports._children = _children; exports._globals = _globals; exports.descendants = descendants; /* helpers which return single identifiers */ exports.exported = exported; exports.parentObject = parentObject; exports._identifier = _identifier; /* helpers which return strings */ exports.anchorName = anchorName; exports.md = md; exports.md2 = md2; exports.methodSig = methodSig; exports.parseLink = parseLink; exports.parentName = parentName; exports.option = option; exports.optionEquals = optionEquals; exports.optionSet = optionSet; exports.optionIsSet = optionIsSet; exports.stripNewlines = stripNewlines; /* helpers which keep state */ exports.headingDepth = headingDepth; exports.depth = depth; exports.depthIncrement = depthIncrement; exports.depthDecrement = depthDecrement; exports.indexDepth = indexDepth; exports.indexDepthIncrement = indexDepthIncrement; exports.indexDepthDecrement = indexDepthDecrement; /** * omits externals without a description * @static */ function _globals (options) { options.hash.scope = 'global' return _identifiers(options).filter(function (identifier) { if (identifier.kind === 'external') { return identifier.description && identifier.description.length > 0 } else { return true } }) } /** * This helper is a duplicate of the handlebars `each` helper with the one exception that * `depthIncrement` is called on each iteration. * @category Block helper: selector */ function children (options) { var context = _children.call(this, options); var fn = options.fn; var inverse = options.inverse; var i = 0; var ret = ''; var data; var contextPath if (options.data) { data = handlebars.createFrame(options.data) } for (var j = context.length; i < j; i++) { depthIncrement(options) if (data) { data.index = i data.first = (i === 0) data.last = (i === (context.length - 1)) if (contextPath) { data.contextPath = contextPath + i } } ret = ret + fn(context[i], { data: data }) depthDecrement(options) } if (i === 0) { ret = inverse(this) } return ret } /** * This helper is a duplicate of the handlebars `each` helper with the one exception that * `indexDepthIncrement` is called on each iteration. * @static * @category Block helper: selector */ function indexChildren (options) { var context = _children.call(this, options) var fn = options.fn var inverse = options.inverse var i = 0 var ret = '' var data var contextPath if (options.data) { data = handlebars.createFrame(options.data) } for (var j = context.length; i < j; i++) { indexDepthIncrement(options) if (data) { data.index = i data.first = (i === 0) data.last = (i === (context.length - 1)) if (contextPath) { data.contextPath = contextPath + i } } ret = ret + fn(context[i], { data: data }) indexDepthDecrement(options) } if (i === 0) { ret = inverse(this) } return ret } /** * @param id {string} - the ID to link to, e.g. `external:XMLHttpRequest`, `GlobalClass#propOne` * etc. * @static * @category Block helper: util * @example * {{#link "module:someModule.property"~}} * {{name}} {{!-- prints 'property' --}} * {{url}} {{!-- prints 'module-someModule-property' --}} * {{/link}} */ function link (longname, options) { return options.fn(_link(longname, options)); } /** * e.g. namepaths `module:Something` or type expression `Array.` * @static * @param {string} - namepath or type expression * @param {object} - the handlebars helper options object */ function _link (input, options) { if (typeof input !== 'string') return null var linked, matches, namepath var output = {} /* test input for 1. A type expression containing a namepath, e.g. Array. 2. a namepath referencing an `id` 3. a namepath referencing a `longname` */ if ((matches = input.match(/.*?<(.*?)>/))) { namepath = matches[1] } else { namepath = input } options.hash = { id: namepath } linked = _identifier(options) if (!linked) { options.hash = { longname: namepath } linked = _identifier(options) } if (!linked) { output = { name: input, url: null } } else { output.name = input.replace(namepath, linked.name) if (isExternal.call(linked)) { if (linked.description) { output.url = '#' + anchorName.call(linked, options) } else { if (linked.see && linked.see.length) { var firstLink = parseLink(linked.see[0])[0] output.url = firstLink ? firstLink.url : linked.see[0] } else { output.url = null } } } else { output.url = '#' + anchorName.call(linked, options) } } return output } /** @static @returns {{symbol: string, types: array}} @category Block helper: util */ function returnSig2 (options) { if (!isConstructor.call(this)) { if (this.returns) { var typeNames = arrayify(this.returns).map(function (ret) { return ret.type && ret.type.names }) typeNames = typeNames.filter(function (name) { return name }) if (typeNames.length) { return options.fn({ symbol: '⇒', types: typeNames.reduce(flatten, []) }) } else { return options.fn({ symbol: null, types: null }) } } else if ((this.type || this.kind === 'namespace') && this.kind !== 'event') { if (this.kind === 'namespace') { return options.fn({ symbol: ':', types: ['object'] }) } else { return options.fn({ symbol: ':', types: this.type.names }) } } } } /** @category Block helper: util */ function sig (options) { var data if (options.data) { data = handlebars.createFrame(options.data || {}) } data.prefix = this.kind === 'constructor' ? 'new' : '' data.parent = null data.accessSymbol = null data.name = isEvent.call(this) ? '"' + this.name + '"' : this.name data.methodSign = null data.returnSymbol = null data.returnTypes = null data.suffix = this.isExported ? '⏏' : isPrivate.call(this) ? '℗' : '' data.depOpen = null data.depClose = null data.codeOpen = null data.codeClose = null var mSig = methodSig.call(this) if (isConstructor.call(this) || isFunction.call(this)) { data.methodSign = '(' + mSig + ')' } else if (isEvent.call(this)) { if (mSig) data.methodSign = '(' + mSig + ')' } if (!isEvent.call(this)) { data.parent = parentName.call(this, options) data.accessSymbol = (this.scope === 'static' || this.scope === 'instance') ? '.' : this.scope === 'inner' ? '~' : '' } if (!isConstructor.call(this)) { if (this.returns) { data.returnSymbol = '⇒' var typeNames = arrayify(this.returns) .map(function (ret) { return ret.type && ret.type.names }) .filter(function (name) { return name }) if (typeNames.length) { data.returnTypes = typeNames.reduce(flatten, []) } } else if ((this.type || this.kind === 'namespace') && this.kind !== 'event') { data.returnSymbol = ':' if (isEnum.call(this)) { data.returnTypes = ['enum'] } else if (this.kind === 'namespace') { data.returnTypes = ['object'] } else { data.returnTypes = this.type.names } } else if (this.chainable) { data.returnSymbol = '↩︎' } else if (this.augments) { data.returnSymbol = '⇐' data.returnTypes = [this.augments[0]] } } if (this.deprecated) { if (optionEquals('no-gfm', true, options) || options.hash['no-gfm']) { data.depOpen = '' data.depClose = '' } else { data.depOpen = '~~' data.depClose = '~~' } } if (option('name-format', options) && !isClass.call(this) && !isModule.call(this)) { data.codeOpen = '`' data.codeClose = '`' } return options.fn(this, { data: data }) } /** @this {identifier} @returns {boolean} @alias module:ddata.isClass */ function isClass () { return this.kind === 'class' } /** returns true if the parent of the current identifier is a class @returns {boolean} @static */ function isClassMember (options) { var parent = arrayify(options.data.root).find(where({ id: this.memberof })) if (parent) { return parent.kind === 'class' } } function isConstructor () { return this.kind === 'constructor' } function isFunction () { return this.kind === 'function' } function isConstant () { return this.kind === 'constant' } /** returns true if this is an event @returns {boolean} @static */ function isEvent () { return this.kind === 'event' } function isEnum () { return this.isEnum || this.kind === 'enum' } function isExternal () { return this.kind === 'external' } function isTypedef () { return this.kind === 'typedef' && this.type.names[0] !== 'function' } function isCallback () { return this.kind === 'typedef' && this.type.names[0] === 'function' } function isModule () { return this.kind === 'module' } function isMixin () { return this.kind === 'mixin' } function isPrivate () { return this.access === 'private' } function isProtected () { return this.access === 'protected' } /** True if there at least two top-level identifiers (i.e. globals or modules) @category returns boolean @returns {boolean} @static */ function showMainIndex (options) { return _orphans(options).length > 1 } /** * Returns an array of the top-level elements which have no parents. Output only includes externals which have a description. * @returns {array} * @static * @category Returns list */ function _orphans (options) { options.hash.memberof = undefined return _identifiers(options).filter(function (identifier) { if (identifier.kind === 'external') { return identifier.description && identifier.description.length > 0 } else { return true } }) } /** * Returns an array of identifiers matching the query * @returns {array} * @static */ function _identifiers (options) { var query = {} for (var prop in options.hash) { if (/^-/.test(prop)) { query[prop.replace(/^-/, '!')] = options.hash[prop] } else if (/^_/.test(prop)) { query[prop.replace(/^_/, '')] = new RegExp(options.hash[prop]) } else { query[prop] = options.hash[prop] } } return arrayify(options.data.root).filter(where(query)).filter(function (doclet) { return !doclet.ignore && (state.options.private ? true : doclet.access !== 'private') }) } /** return the identifiers which are a `memberof` this one. Exclude externals without descriptions. @param [sortBy] {string} - "kind" @param [min] {number} - only returns if there are `min` children @this {identifier} @returns {identifier[]} @static */ function _children (options) { if (!this.id) return [] var min = options.hash.min delete options.hash.min options.hash.memberof = this.id var output = _identifiers(options) output = output.filter(function (identifier) { if (identifier.kind === 'external') { return identifier.description && identifier.description.length > 0 } else { return true } }) if (output.length >= (min || 0)) return output } /** return a flat list containing all decendants @param [sortBy] {string} - "kind" @param [min] {number} - only returns if there are `min` children @this {identifier} @returns {identifier[]} @static */ function descendants (options) { var min = typeof options.hash.min !== 'undefined' ? options.hash.min : 2 delete options.hash.min options.hash.memberof = this.id var output = [] function iterate (childrenList) { if (childrenList.length) { childrenList.forEach(function (child) { output.push(child) iterate(_children.call(child, options)) }) } } iterate(_children.call(this, options)) if (output.length >= (min || 0)) return output } /** returns the exported identifier of this module @this {identifier} - only works on a module @returns {identifier} @static */ function exported (options) { var exp = arrayify(options.data.root).find(where({ '!kind': 'module', id: this.id })) return exp || this } /** Returns an identifier matching the query @static */ function _identifier (options) { return _identifiers(options)[0] } /** Returns the parent @static */ function parentObject (options) { return arrayify(options.data.root).find(where({ id: this.memberof })) } /** returns a unique ID string suitable for use as an `href`. @this {identifier} @returns {string} @static @category Returns string @example ```js > ddata.anchorName.call({ id: "module:yeah--Yeah()" }) 'module_yeah--Yeah_new' ``` */ function anchorName (options) { if (!this.id) throw new Error('[anchorName helper] cannot create a link without a id: ' + JSON.stringify(this)) if (this.inherited) { options.hash.id = this.inherits var inherits = _identifier(options) if (inherits) { return anchorName.call(inherits, options) } else { return '' } } return util.format( '%s%s%s', this.isExported ? 'exp_' : '', this.kind === 'constructor' ? 'new_' : '', this.id .replace(/:/g, '_') .replace(/~/g, '..') .replace(/\(\)/g, '_new') .replace(/#/g, '+') ) } /** converts the supplied text to markdown @static @category Returns string */ function md (string, options) { if (string) { var result = marked(string).replace('lang-js', 'language-javascript') return result } } function md2 (options) { return marked.inlineLexer(options.fn(this).toString(), []) } /** Returns the method signature, e.g. `(options, [onComplete])` @this {identifier} @returns {string} @category Returns string @static */ function methodSig () { return arrayify(this.params).filter(function (param) { return param.name && !/\./.test(param.name); }).map(function (param) { if (param.variable) { return param.optional ? "[..." + param.name + "]" : "..." + param.name; } else { return param.optional ? "[" + param.name + "]" : param.name; } }).join(", "); } /** * extracts url and caption data from @link tags * @param {string} - a string containing one or more {@link} tags * @returns {Array.<{original: string, caption: string, url: string}>} * @static */ function parseLink (text) { if (!text) return '' var results = [] var matches = null var link1 = /{@link\s+([^\s}|]+?)\s*}/g // {@link someSymbol} var link2 = /\[([^\]]+?)\]{@link\s+([^\s}|]+?)\s*}/g // [caption here]{@link someSymbol} var link3 = /{@link\s+([^\s}|]+?)\s*\|([^}]+?)}/g // {@link someSymbol|caption here} var link4 = /{@link\s+([^\s}|]+?)\s+([^}|]+?)}/g // {@link someSymbol Caption Here} while ((matches = link4.exec(text)) !== null) { results.push({ original: matches[0], caption: matches[2], url: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } while ((matches = link3.exec(text)) !== null) { results.push({ original: matches[0], caption: matches[2], url: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } while ((matches = link2.exec(text)) !== null) { results.push({ original: matches[0], caption: matches[1], url: matches[2] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } while ((matches = link1.exec(text)) !== null) { results.push({ original: matches[0], caption: matches[1], url: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } return results } /** returns the parent name, instantiated if necessary @this {identifier} @returns {string} @static */ function parentName (options) { function instantiate (input) { if (/^[A-Z]{3}/.test(input)) { return input.replace(/^([A-Z]+)([A-Z])/, function (str, p1, p2) { return p1.toLowerCase() + p2 }) } else { return input.charAt(0).toLowerCase() + input.slice(1) } } /* don't bother with a parentName for exported identifiers */ if (this.isExported) return '' if (this.memberof && this.kind !== 'constructor') { var parent = arrayify(options.data.root).find(where({ id: this.memberof })) if (parent) { if (this.scope === 'instance') { var name = parent.typicalname || parent.name return instantiate(name) } else if (this.scope === 'static' && !(parent.kind === 'class' || parent.kind === 'constructor')) { return parent.typicalname || parent.name } else { return parent.name } } else { return this.memberof } } } /** returns a dmd option, e.g. "sort-by", "heading-depth" etc. @static */ function option (name, options) { return objectGet(options.data.root.options, name) } /** @static */ function optionEquals (name, value, options) { return options.data.root.options[name] === value } /** @static */ function optionSet (name, value, options) { options.data.root.options[name] = value } /** @static */ function optionIsSet (name, options) { return options.data.root.options[name] !== undefined } /** @static */ function stripNewlines (input) { if (input) return input.replace(/[\r\n]+/g, ' ') } /** @static */ function headingDepth (options) { return options.data.root.options._depth + (options.data.root.options['heading-depth']) } /** @static */ function depth (options) { return options.data.root.options._depth } /** @static */ function depthIncrement (options) { options.data.root.options._depth++ } /** @static */ function depthDecrement (options) { options.data.root.options._depth-- } /** @static */ function indexDepth (options) { return options.data.root.options._indexDepth } /** @static */ function indexDepthIncrement (options) { options.data.root.options._indexDepth++ } /** @static */ function indexDepthDecrement (options) { options.data.root.options._indexDepth-- } ================================================ FILE: scripts/api-documentation/templates/helpers/djip-helpers.js ================================================ const ddata = require("./ddata.js"); /** * Strips newline characters (\n) from the input * @param input {string} * @returns {string} */ function stripNewlines (input) { if (input) return input.replace(/[\r\n]+/g, " "); } exports.stripNewlines = stripNewlines; /** * Extract the event name from the jsdoc-reported name * @param input {string} * @returns {string} */ function eventName (input) { return input.split(":")[1]; } exports.eventName = eventName; /** * Replaces JSDoc {@link} tags with markdown links in the supplied text */ function inlineLinks (text, options) { if (text) { const links = ddata.parseLink(text); links.forEach(function (link) { const linked = ddata._link(link.url, options); if (link.caption === link.url) link.caption = linked.name; if (linked.url) link.url = linked.url; text = text.replace(link.original, `[${link.caption}](${link.url})`); }); } return text; } exports.inlineLinks = inlineLinks; function curly(object, open) { return open ? "{" : "}"; }; exports.curly = curly; function eq(v1, v2) { return v1 === v2; } exports.eq = eq; function ne(v1, v2) { return v1 !== v2; } exports.ne = ne; function lt(v1, v2) {return v1 < v2; } exports.lt = lt; function gt(v1, v2) { return v1 > v2; } exports.gt = gt; function lte(v1, v2) { return v1 <= v2; } exports.lte = lte; function gte(v1, v2) { return v1 >= v2; } exports.gte = gte; function and() { return Array.prototype.every.call(arguments, Boolean); } exports.and = and; function or() { return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); } exports.or = or; function methodSignature(context) { return ddata.methodSig.call(context); } exports.methodSignature = methodSignature; function createEventAnchor(name) { return "#event-" + name.replace(":", "-"); } exports.createEventAnchor = createEventAnchor; ================================================ FILE: scripts/api-documentation/templates/helpers/state.js ================================================ exports.templateData = [] exports.options = {} ================================================ FILE: scripts/library/build.js ================================================ // This script builds the bundled library files (both normal and minified with sourcemaps) and // commits them to the 'dist' folder for later publishing. By default, CommonJS, ES Modules and IIFE // versions are built. // // Calling this script with the -t argument allows building only of them. Options are: cjs, esm and // iife. // Modules const system = require("system-commands"); const fs = require("fs"); const path = require("path"); // Parse arguments (default type is esm). Use -t as type (if valid) let type = "esm"; const argv = require("minimist")(process.argv.slice(2)); if (["cjs", "esm", "iife"].includes(argv.t)) type = argv.t; // Prepare general command let cmd = `./node_modules/.bin/rollup ` + `--input src/WebMidi.js ` + `--format ${type} `; // Minified version (with sourcemap) let minified = cmd + ` --file dist/${type}/webmidi.${type}.min.js ` + `--sourcemap ` + `--config ${__dirname}/rollup.config.${type}.min.js`; // Non-minified version let normal = cmd + ` --file dist/${type}/webmidi.${type}.js ` + `--config ${__dirname}/rollup.config.${type}.js`; async function execute(type) { // Write package.json file so proper versions are imported by Node if (type === "esm") { fs.writeFileSync( path.join(process.cwd(), "dist", "esm", "package.json"), '{"type": "module"}' ); console.info( "\x1b[32m", // green font `Custom package.json file ("type": "module") saved to "dist/${type}/package.json"`, "\x1b[0m" // reset font ); } else if (type === "cjs") { fs.writeFileSync( path.join(process.cwd(), "dist", "cjs", "package.json"), '{"type": "commonjs"}' ); console.info( "\x1b[32m", // green font `Custom package.json file ("type": "commonjs") saved to "dist/${type}/package.json"`, "\x1b[0m" // reset font ); } // Production build await system(minified); console.info( "\x1b[32m", // green font `The "${type}" minified build was saved to "dist/${type}/webmidi.${type}.min.js"`, "\x1b[0m" // reset font ); // Development build await system(normal); console.info( "\x1b[32m", // green font `The "${type}" non-minified build was saved to "dist/${type}/webmidi.${type}.js"`, "\x1b[0m" // reset font ); } // Execute and catch errors if any (in red) execute(type).catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); ================================================ FILE: scripts/library/rollup.config.cjs.js ================================================ import babel from "@rollup/plugin-babel"; import stripCode from "rollup-plugin-strip-code"; import replace from "@rollup/plugin-replace"; const fs = require("fs"); const license = require("rollup-plugin-license"); const versionInjector = require("rollup-plugin-version-injector"); const BANNER = fs.readFileSync(__dirname + "/../../BANNER.txt", "utf8") + "\n\n\n"; export default { plugins: [ versionInjector(), replace({__flavour__: "cjs"}), stripCode({ start_comment: "START-ESM", end_comment: "END-ESM" }), babel(), license({banner: BANNER}) ] }; ================================================ FILE: scripts/library/rollup.config.esm.js ================================================ import stripCode from "rollup-plugin-strip-code"; import replace from "@rollup/plugin-replace"; const fs = require("fs"); const license = require("rollup-plugin-license"); const versionInjector = require("rollup-plugin-version-injector"); const BANNER = fs.readFileSync(__dirname + "/../../BANNER.txt", "utf8") + "\n\n\n"; export default { plugins: [ versionInjector(), replace({__flavour__: "esm"}), stripCode({ start_comment: "START-CJS", end_comment: "END-CJS" }), license({ banner: BANNER }) ] }; ================================================ FILE: scripts/library/rollup.config.iife.js ================================================ import babel from "@rollup/plugin-babel"; import stripCode from "rollup-plugin-strip-code"; import replace from "@rollup/plugin-replace"; const fs = require("fs"); const license = require("rollup-plugin-license"); const versionInjector = require("rollup-plugin-version-injector"); const BANNER = fs.readFileSync(__dirname + "/../../BANNER.txt", "utf8") + "\n\n\n"; export default { output: { name: "window", // WebMidi and Note will be added to window extend: true, // important! exports: "named" }, plugins: [ versionInjector(), replace({__flavour__: "iife"}), stripCode({ start_comment: "START-CJS", end_comment: "END-CJS" }), stripCode({ start_comment: "START-ESM", end_comment: "END-ESM" }), babel(), license({ banner: BANNER }) ] }; ================================================ FILE: scripts/sponsors/retrieve-sponsors.js ================================================ const { graphql } = require("@octokit/graphql"); const { token } = require("../../.credentials/sponsors.js"); const replace = require("replace-in-file"); const path = require("path"); const TARGET = path.join(process.cwd(), "website", "src", "pages", "sponsors", "index.md"); async function getSponsors() { const query = `{ user (login: "djipco") { sponsorshipsAsMaintainer(first: 100, includePrivate: true) { totalCount nodes { sponsorEntity { ... on User { login name avatarUrl url sponsorshipForViewerAsSponsorable { isOneTimePayment privacyLevel tier { id name monthlyPriceInDollars } } } } } } } }`; const {user} = await graphql( query, { headers: { authorization: `token ${token}` } } ); return user.sponsorshipsAsMaintainer.nodes; } getSponsors().then(async data => { // data.forEach(d => { // console.log(d.sponsorEntity.login, d.sponsorEntity.sponsorshipForViewerAsSponsorable); // // console.log(d); // }); let output = ""; data.sort((a, b) => { a = a.sponsorEntity.sponsorshipForViewerAsSponsorable.tier.monthlyPriceInDollars; b = b.sponsorEntity.sponsorshipForViewerAsSponsorable.tier.monthlyPriceInDollars; if (a > b) return -1; if (a < b) return -1; return 0; }); data.forEach(entity => { const sponsor = entity.sponsorEntity; if (sponsor.sponsorshipForViewerAsSponsorable.tier.monthlyPriceInDollars < 6) { return; } if (sponsor.sponsorshipForViewerAsSponsorable.privacyLevel === "PUBLIC") { const name = sponsor.name || sponsor.login; output += `\n`; output += `\t\n`; output += `\n\n`; } else { output += `\n\n`; } }); const options = { files: TARGET, from: /.*/sgm, to: "\n\n" + output + "", }; const results = await replace(options); }); ================================================ FILE: scripts/typescript-declarations/generate.js ================================================ // This script injects a few dynamic properties into the source TypeScript declaration file and // copies it to the ./dist/esm and ./dist/cjs directories. // Modules const path = require("path"); const replace = require("replace-in-file"); const pkg = require("../../package.json"); const fs = require("fs-extra"); // Path to source declaration file const SOURCE_FILE = path.join(__dirname, "../../typescript", "webmidi.d.ts"); // Output directory const OUTPUT_DIR = "dist"; // Console arguments const argv = require("minimist")(process.argv.slice(2)); // Get targets to save the file for let targets = []; let type = "all"; if (["cjs", "esm"].includes(argv.t)) type = argv.t; if (type === "all" || type === "cjs") targets.push({path: "webmidi.cjs.d.ts", name: "cjs", type: "CommonJS"}); if (type === "all" || type === "esm") targets.push({path: "webmidi.esm.d.ts", name: "esm", type: "ES2020"}); async function execute() { targets.forEach(async target => { const FILE = path.join(OUTPUT_DIR, target.name, target.path); // Copy the file to the target directory await fs.copy(SOURCE_FILE, FILE, {overwrite: true}); // Insert dynamic data replace.sync({ files: FILE, from: ["{{LIBRARY}}", "{{VERSION}}", "{{HOMEPAGE}}", "{{AUTHOR_NAME}}", "{{AUTHOR_URL}}"], to: [pkg.webmidi.name, pkg.version, pkg.homepage, pkg.author.name, pkg.author.url] }); log(`Saved ${target.type} TypeScript declaration file to '${FILE}'`); }); } // Execute and catch errors if any (in red) execute().catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); function log(message) { console.info("\x1b[32m", message, "\x1b[0m"); } ================================================ FILE: scripts/typescript-declarations/generateOLD.js ================================================ // This script generates TypeScript declaration files (.d.ts) of the library and saves them to the // 'dist' directory for later publishing. By default, CommonJS and ES2020 versions are generated. // // Calling this script with the -t argument allows generating only one declaration file. Options // are: cjs and esm. // // Calling this script with the -c argument allows you to commit and push the generated files. // Options are true or false. // Modules const git = require("simple-git")(); const moment = require("moment"); const path = require("path"); const prependFile = require("prepend-file"); const replace = require("replace-in-file"); const system = require("system-commands"); const pkg = require("../../package.json"); const os = require("os"); const fsPromises = require("fs").promises; const fs = require("fs-extra"); const OUT_DIR = "dist"; const WEB_MIDI_API_CLASSES = [ "MIDIAccess", "MIDIConnectionEvent", "MIDIConnectionEventInit", "MIDIInput", "MIDIInputMap", "MIDIMessageEvent", "MIDIMessageEventInit", "MIDIOptions", "MIDIOutput", "MIDIOutputMap", "MIDIPort", "MIDIPortConnectionStatus", "MIDIPortDeviceState", "MIDIPortType" ]; const HEADER = `// Type definitions for ${pkg.webmidi.name} ${pkg.version}\n` + `// Project: ${pkg.homepage}\n` + `// Definitions by: ${pkg.author.name} \n` + `// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped\n\n`; let targets = []; let type = "all"; const argv = require("minimist")(process.argv.slice(2)); if (["cjs", "esm"].includes(argv.t)) type = argv.t; const commit = argv.c === "true"; if (type === "all" || type === "cjs") targets.push({source: "webmidi.cjs.js", name: "cjs", type: "CommonJS"}); if (type === "all" || type === "esm") targets.push({source: "webmidi.esm.js", name: "esm", type: "ES2020"}); async function execute() { // Temp dir const TMP_DIR_PATH = await fsPromises.mkdtemp(path.join(os.tmpdir(), "webmidi-ts-")); targets.forEach(async target => { // Make a copy of the source file so we can substitute some elements before parsing const TMP_FILE_PATH = path.join(TMP_DIR_PATH, target.source); await fs.copy( path.join(OUT_DIR, target.name, target.source), TMP_FILE_PATH, {overwrite: true} ); // Add TypeScript WebMidi namespace before native Web MIDI API objects WEB_MIDI_API_CLASSES.forEach(element => { const options = { files: TMP_FILE_PATH, from: new RegExp("{" + element + "}", "g"), to: () => `{WebMidi.${element}}` }; replace.sync(options); }); // Replace callback type by simply "EventEmitterCallback" (this is needed because tsc does not // support tilde character in types. See below... replace.sync({ files: TMP_FILE_PATH, from: new RegExp("{EventEmitter~callback}", "g"), to: () => "{EventEmitterCallback}" }); // Generate declaration file const cmd = "npx -p typescript tsc " + TMP_FILE_PATH + " --declaration --allowJs --emitDeclarationOnly" + " --module " + target.type + " --lib ES2020,DOM --outDir " + path.join(OUT_DIR, target.name) ; await system(cmd); // Path to current .d.ts file const file = path.join(OUT_DIR, target.name, target.source.replace(".js", ".d.ts")); // Remove readonly flag const options = { files: [file], from: /readonly /g, to: "" }; await replace(options); // // Inject correct definitions for Input class // await replace({ // files: [file], // from: /export class Input extends EventEmitter \{/, // to: fs.readFileSync( // path.join(__dirname, "injections", "Input.txt"), {encoding: "utf8", flag: "r"} // ) // }); // // Prepend header for DefinitelyTyped await prependFile(file, HEADER); log("Saved " + target.type + " TypeScript declaration file to '" + file + "'"); // // Inject EventEmitterCallback type declaration // fs.appendFileSync( // file, // `\n\n` + // `// This is automatically injected to fix a bug with the TypeScript compiler\n` + // `export type EventEmitterCallback = (...args: any[]) => void;\n` // // dans Tone.js, ils utilisent: // //`export type EventEmitterCallback = (...args: A) => void;` // ); // Commit if (commit) { await git.add([file]); await git.commit("Generated by TypeScript compiler on " + moment().format()); await git.push(); } }); } // Execute and catch errors if any (in red) execute().catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); function log(message) { console.info("\x1b[32m", message, "\x1b[0m"); } ================================================ FILE: scripts/website/deploy.js ================================================ // This script copies the files generated by Docusaurus in /website/build to the root of the // 'gh-pages' branch. This makes them available at djipco.github.io/webmidi // Importation const fs = require("fs-extra"); const fsPromises = require("fs").promises; const git = require("simple-git")(); const moment = require("moment"); const os = require("os"); const path = require("path"); const rimraf = require("@alexbinary/rimraf"); // Configuration const TARGET_BRANCH = "gh-pages"; const SOURCE_DIR = path.join(process.cwd(), "website", "build"); async function execute() { const TMP_DIR = await fsPromises.mkdtemp(path.join(os.tmpdir(), "webmidi-website-")); // Get list of files and directories inside the Docusaurus build directory let files = await fsPromises.readdir(SOURCE_DIR); // Get rid of .DS_Store and other unnecessary files files = files.filter(file => !file.startsWith(".")); // Copy files to tmp directory for (const file of files) { const source = path.join(SOURCE_DIR, file); const target = path.join(TMP_DIR, file); await fs.copy(source, target, {overwrite: true}); } log("Copied website files to temp dir: " + TMP_DIR); // Get current branch (so we can come back to it later) let results = await git.branch(); const ORIGINAL_BRANCH = results.current; // Switch to target branch log(`Switching from '${ORIGINAL_BRANCH}' branch to '${TARGET_BRANCH}' branch`); await git.checkout(TARGET_BRANCH); // Copy content of tmp dir to final destination and commit for (const file of files) { const source = path.join(TMP_DIR, file); const target = path.join(process.cwd(), file); await fs.copy(source, target, {overwrite: true}); await git.add([target]); await git.commit("Updated on: " + moment().format(), [target]); } await git.push(); log(`Website files committed to '${TARGET_BRANCH}' branch and pushed to remote`); // Remove temp files rimraf(TMP_DIR); log(`Temp directory removed`); // Come back to original branch log(`Switching back to '${ORIGINAL_BRANCH}' branch`); await git.checkout(ORIGINAL_BRANCH); } function log(message) { console.info("\x1b[32m", message, "\x1b[0m"); } // Execution execute().catch(error => console.error("\x1b[31m", "Error: " + error, "\x1b[0m")); ================================================ FILE: src/Enumerations.js ================================================ /** * The `Enumerations` class contains enumerations and arrays of elements used throughout the * library. All its properties are static and should be referenced using the class name. For * example: `Enumerations.CHANNEL_MESSAGES`. * * @license Apache-2.0 * @since 3.0.0 */ export class Enumerations { /** * @enum {Object.} * @readonly * @deprecated since 3.1 (use Enumerations.CHANNEL_MESSAGES instead) * @private * @static */ static get MIDI_CHANNEL_MESSAGES() { if (this.validation) { console.warn( "The MIDI_CHANNEL_MESSAGES enum has been deprecated. Use the " + "Enumerations.CHANNEL_MESSAGES enum instead." ); } return Enumerations.CHANNEL_MESSAGES; } /** * Enumeration of all MIDI channel message names and their associated 4-bit numerical value: * * | Message Name | Hexadecimal | Decimal | * |---------------------|-------------|---------| * | `noteoff` | 0x8 | 8 | * | `noteon` | 0x9 | 9 | * | `keyaftertouch` | 0xA | 10 | * | `controlchange` | 0xB | 11 | * | `programchange` | 0xC | 12 | * | `channelaftertouch` | 0xD | 13 | * | `pitchbend` | 0xE | 14 | * * @enum {Object.} * @readonly * @since 3.1 * @static */ static get CHANNEL_MESSAGES() { return { noteoff: 0x8, // 8 noteon: 0x9, // 9 keyaftertouch: 0xA, // 10 controlchange: 0xB, // 11 programchange: 0xC, // 12 channelaftertouch: 0xD, // 13 pitchbend: 0xE // 14 }; } /** * A simple array of the 16 valid MIDI channel numbers (`1` to `16`): * * @type {number[]} * @readonly * @since 3.1 * @static */ static get CHANNEL_NUMBERS() { return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; } /** * @type {number[]} * @readonly * @deprecated since 3.1 (use Enumerations.CHANNEL_NUMBERS instead) * @private * @static */ static get MIDI_CHANNEL_NUMBERS() { if (this.validation) { console.warn( "The MIDI_CHANNEL_NUMBERS array has been deprecated. Use the " + "Enumerations.CHANNEL_NUMBERS array instead." ); } return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; } /** * Enumeration of all MIDI channel mode message names and their associated numerical value: * * * | Message Name | Hexadecimal | Decimal | * |-----------------------|-------------|---------| * | `allsoundoff` | 0x78 | 120 | * | `resetallcontrollers` | 0x79 | 121 | * | `localcontrol` | 0x7A | 122 | * | `allnotesoff` | 0x7B | 123 | * | `omnimodeoff` | 0x7C | 124 | * | `omnimodeon` | 0x7D | 125 | * | `monomodeon` | 0x7E | 126 | * | `polymodeon` | 0x7F | 127 | * * @enum {Object.} * @readonly * @since 3.1 * @static */ static get CHANNEL_MODE_MESSAGES() { return { allsoundoff: 120, resetallcontrollers: 121, localcontrol: 122, allnotesoff: 123, omnimodeoff: 124, omnimodeon: 125, monomodeon: 126, polymodeon: 127 }; } /** * @enum {Object.} * @deprecated since 3.1 (use Enumerations.CHANNEL_MODE_MESSAGES instead) * @private * @readonly * @static */ static get MIDI_CHANNEL_MODE_MESSAGES() { if (this.validation) { console.warn( "The MIDI_CHANNEL_MODE_MESSAGES enum has been deprecated. Use the " + "Enumerations.CHANNEL_MODE_MESSAGES enum instead." ); } return Enumerations.CHANNEL_MODE_MESSAGES; } /** * @enum {Object.} * @readonly * @static * @private * @deprecated since version 3.0.26 (use `CONTROL_CHANGE_MESSAGES` instead) */ static get MIDI_CONTROL_CHANGE_MESSAGES() { if (this.validation) { console.warn( "The MIDI_CONTROL_CHANGE_MESSAGES enum has been deprecated. Use the " + "Enumerations.CONTROL_CHANGE_MESSAGES array instead." ); } return { bankselectcoarse: 0, modulationwheelcoarse: 1, breathcontrollercoarse: 2, controller3: 3, footcontrollercoarse: 4, portamentotimecoarse: 5, dataentrycoarse: 6, volumecoarse: 7, balancecoarse: 8, controller9: 9, pancoarse: 10, expressioncoarse: 11, effectcontrol1coarse: 12, effectcontrol2coarse: 13, controller14: 14, controller15: 15, generalpurposeslider1: 16, generalpurposeslider2: 17, generalpurposeslider3: 18, generalpurposeslider4: 19, controller20: 20, controller21: 21, controller22: 22, controller23: 23, controller24: 24, controller25: 25, controller26: 26, controller27: 27, controller28: 28, controller29: 29, controller30: 30, controller31: 31, bankselectfine: 32, modulationwheelfine: 33, breathcontrollerfine: 34, controller35: 35, footcontrollerfine: 36, portamentotimefine: 37, dataentryfine: 38, volumefine: 39, balancefine: 40, controller41: 41, panfine: 42, expressionfine: 43, effectcontrol1fine: 44, effectcontrol2fine: 45, controller46: 46, controller47: 47, controller48: 48, controller49: 49, controller50: 50, controller51: 51, controller52: 52, controller53: 53, controller54: 54, controller55: 55, controller56: 56, controller57: 57, controller58: 58, controller59: 59, controller60: 60, controller61: 61, controller62: 62, controller63: 63, holdpedal: 64, portamento: 65, sustenutopedal: 66, softpedal: 67, legatopedal: 68, hold2pedal: 69, soundvariation: 70, resonance: 71, soundreleasetime: 72, soundattacktime: 73, brightness: 74, soundcontrol6: 75, soundcontrol7: 76, soundcontrol8: 77, soundcontrol9: 78, soundcontrol10: 79, generalpurposebutton1: 80, generalpurposebutton2: 81, generalpurposebutton3: 82, generalpurposebutton4: 83, controller84: 84, controller85: 85, controller86: 86, controller87: 87, controller88: 88, controller89: 89, controller90: 90, reverblevel: 91, tremololevel: 92, choruslevel: 93, celestelevel: 94, phaserlevel: 95, databuttonincrement: 96, databuttondecrement: 97, nonregisteredparametercoarse: 98, nonregisteredparameterfine: 99, registeredparametercoarse: 100, registeredparameterfine: 101, controller102: 102, controller103: 103, controller104: 104, controller105: 105, controller106: 106, controller107: 107, controller108: 108, controller109: 109, controller110: 110, controller111: 111, controller112: 112, controller113: 113, controller114: 114, controller115: 115, controller116: 116, controller117: 117, controller118: 118, controller119: 119, allsoundoff: 120, resetallcontrollers: 121, localcontrol: 122, allnotesoff: 123, omnimodeoff: 124, omnimodeon: 125, monomodeon: 126, polymodeon: 127 }; } /** * An array of objects, ordered by control number, describing control change messages. Each object * in the array has 3 properties with some objects having a fourth one (`position`) : * * * `number`: MIDI control number (0-127); * * `name`: name of emitted event (eg: `bankselectcoarse`, `choruslevel`, etc) that can be * listened to; * * `description`: user-friendly description of the controller's purpose; * * `position` (optional): whether this controller's value should be considered an `msb` or * `lsb` * * Not all controllers have a predefined function. For those that don't, `name` is the word * "controller" followed by the number (e.g. `controller112`). * * | Event name | Control Number | * |--------------------------------|----------------| * | `bankselectcoarse` | 0 | * | `modulationwheelcoarse` | 1 | * | `breathcontrollercoarse` | 2 | * | `controller3` | 3 | * | `footcontrollercoarse` | 4 | * | `portamentotimecoarse` | 5 | * | `dataentrycoarse` | 6 | * | `volumecoarse` | 7 | * | `balancecoarse` | 8 | * | `controller9` | 9 | * | `pancoarse` | 10 | * | `expressioncoarse` | 11 | * | `effectcontrol1coarse` | 12 | * | `effectcontrol2coarse` | 13 | * | `controller14` | 14 | * | `controller15` | 15 | * | `generalpurposecontroller1` | 16 | * | `generalpurposecontroller2` | 17 | * | `generalpurposecontroller3` | 18 | * | `generalpurposecontroller4` | 19 | * | `controller20` | 20 | * | `controller21` | 21 | * | `controller22` | 22 | * | `controller23` | 23 | * | `controller24` | 24 | * | `controller25` | 25 | * | `controller26` | 26 | * | `controller27` | 27 | * | `controller28` | 28 | * | `controller29` | 29 | * | `controller30` | 30 | * | `controller31` | 31 | * | `bankselectfine` | 32 | * | `modulationwheelfine` | 33 | * | `breathcontrollerfine` | 34 | * | `controller35` | 35 | * | `footcontrollerfine` | 36 | * | `portamentotimefine` | 37 | * | `dataentryfine` | 38 | * | `channelvolumefine` | 39 | * | `balancefine` | 40 | * | `controller41` | 41 | * | `panfine` | 42 | * | `expressionfine` | 43 | * | `effectcontrol1fine` | 44 | * | `effectcontrol2fine` | 45 | * | `controller46` | 46 | * | `controller47` | 47 | * | `controller48` | 48 | * | `controller49` | 49 | * | `controller50` | 50 | * | `controller51` | 51 | * | `controller52` | 52 | * | `controller53` | 53 | * | `controller54` | 54 | * | `controller55` | 55 | * | `controller56` | 56 | * | `controller57` | 57 | * | `controller58` | 58 | * | `controller59` | 59 | * | `controller60` | 60 | * | `controller61` | 61 | * | `controller62` | 62 | * | `controller63` | 63 | * | `damperpedal` | 64 | * | `portamento` | 65 | * | `sostenuto` | 66 | * | `softpedal` | 67 | * | `legatopedal` | 68 | * | `hold2` | 69 | * | `soundvariation` | 70 | * | `resonance` | 71 | * | `releasetime` | 72 | * | `attacktime` | 73 | * | `brightness` | 74 | * | `decaytime` | 75 | * | `vibratorate` | 76 | * | `vibratodepth` | 77 | * | `vibratodelay` | 78 | * | `controller79` | 79 | * | `generalpurposecontroller5` | 80 | * | `generalpurposecontroller6` | 81 | * | `generalpurposecontroller7` | 82 | * | `generalpurposecontroller8` | 83 | * | `portamentocontrol` | 84 | * | `controller85` | 85 | * | `controller86` | 86 | * | `controller87` | 87 | * | `highresolutionvelocityprefix` | 88 | * | `controller89` | 89 | * | `controller90` | 90 | * | `effect1depth` | 91 | * | `effect2depth` | 92 | * | `effect3depth` | 93 | * | `effect4depth` | 94 | * | `effect5depth` | 95 | * | `dataincrement` | 96 | * | `datadecrement` | 97 | * | `nonregisteredparameterfine` | 98 | * | `nonregisteredparametercoarse` | 99 | * | `nonregisteredparameterfine` | 100 | * | `registeredparametercoarse` | 101 | * | `controller102` | 102 | * | `controller103` | 103 | * | `controller104` | 104 | * | `controller105` | 105 | * | `controller106` | 106 | * | `controller107` | 107 | * | `controller108` | 108 | * | `controller109` | 109 | * | `controller110` | 110 | * | `controller111` | 111 | * | `controller112` | 112 | * | `controller113` | 113 | * | `controller114` | 114 | * | `controller115` | 115 | * | `controller116` | 116 | * | `controller117` | 117 | * | `controller118` | 118 | * | `controller119` | 119 | * | `allsoundoff` | 120 | * | `resetallcontrollers` | 121 | * | `localcontrol` | 122 | * | `allnotesoff` | 123 | * | `omnimodeoff` | 124 | * | `omnimodeon` | 125 | * | `monomodeon` | 126 | * | `polymodeon` | 127 | * * @type {object[]} * @readonly * @static * @since 3.1 */ static get CONTROL_CHANGE_MESSAGES() { return [ { number: 0, name: "bankselectcoarse", description: "Bank Select (Coarse)", position: "msb" }, { number: 1, name: "modulationwheelcoarse", description: "Modulation Wheel (Coarse)", position: "msb" }, { number: 2, name: "breathcontrollercoarse", description: "Breath Controller (Coarse)", position: "msb" }, { number: 3, name: "controller3", description: "Undefined", position: "msb" }, { number: 4, name: "footcontrollercoarse", description: "Foot Controller (Coarse)", position: "msb" }, { number: 5, name: "portamentotimecoarse", description: "Portamento Time (Coarse)", position: "msb" }, { number: 6, name: "dataentrycoarse", description: "Data Entry (Coarse)", position: "msb" }, { number: 7, name: "volumecoarse", description: "Channel Volume (Coarse)", position: "msb" }, { number: 8, name: "balancecoarse", description: "Balance (Coarse)", position: "msb" }, { number: 9, name: "controller9", description: "Controller 9 (Coarse)", position: "msb" }, { number: 10, name: "pancoarse", description: "Pan (Coarse)", position: "msb" }, { number: 11, name: "expressioncoarse", description: "Expression Controller (Coarse)", position: "msb" }, { number: 12, name: "effectcontrol1coarse", description: "Effect Control 1 (Coarse)", position: "msb" }, { number: 13, name: "effectcontrol2coarse", description: "Effect Control 2 (Coarse)", position: "msb" }, { number: 14, name: "controller14", description: "Undefined", position: "msb" }, { number: 15, name: "controller15", description: "Undefined", position: "msb" }, { number: 16, name: "generalpurposecontroller1", description: "General Purpose Controller 1 (Coarse)", position: "msb" }, { number: 17, name: "generalpurposecontroller2", description: "General Purpose Controller 2 (Coarse)", position: "msb" }, { number: 18, name: "generalpurposecontroller3", description: "General Purpose Controller 3 (Coarse)", position: "msb" }, { number: 19, name: "generalpurposecontroller4", description: "General Purpose Controller 4 (Coarse)", position: "msb" }, { number: 20, name: "controller20", description: "Undefined", position: "msb" }, { number: 21, name: "controller21", description: "Undefined", position: "msb" }, { number: 22, name: "controller22", description: "Undefined", position: "msb" }, { number: 23, name: "controller23", description: "Undefined", position: "msb" }, { number: 24, name: "controller24", description: "Undefined", position: "msb" }, { number: 25, name: "controller25", description: "Undefined", position: "msb" }, { number: 26, name: "controller26", description: "Undefined", position: "msb" }, { number: 27, name: "controller27", description: "Undefined", position: "msb" }, { number: 28, name: "controller28", description: "Undefined", position: "msb" }, { number: 29, name: "controller29", description: "Undefined", position: "msb" }, { number: 30, name: "controller30", description: "Undefined", position: "msb" }, { number: 31, name: "controller31", description: "Undefined", position: "msb" }, { number: 32, name: "bankselectfine", description: "Bank Select (Fine)", position: "lsb" }, { number: 33, name: "modulationwheelfine", description: "Modulation Wheel (Fine)", position: "lsb" }, { number: 34, name: "breathcontrollerfine", description: "Breath Controller (Fine)", position: "lsb" }, { number: 35, name: "controller35", description: "Undefined", position: "lsb" }, { number: 36, name: "footcontrollerfine", description: "Foot Controller (Fine)", position: "lsb" }, { number: 37, name: "portamentotimefine", description: "Portamento Time (Fine)", position: "lsb" }, { number: 38, name: "dataentryfine", description: "Data Entry (Fine)", position: "lsb" }, { number: 39, name: "channelvolumefine", description: "Channel Volume (Fine)", position: "lsb" }, { number: 40, name: "balancefine", description: "Balance (Fine)", position: "lsb" }, { number: 41, name: "controller41", description: "Undefined", position: "lsb" }, { number: 42, name: "panfine", description: "Pan (Fine)", position: "lsb" }, { number: 43, name: "expressionfine", description: "Expression Controller (Fine)", position: "lsb" }, { number: 44, name: "effectcontrol1fine", description: "Effect control 1 (Fine)", position: "lsb" }, { number: 45, name: "effectcontrol2fine", description: "Effect control 2 (Fine)", position: "lsb" }, { number: 46, name: "controller46", description: "Undefined", position: "lsb" }, { number: 47, name: "controller47", description: "Undefined", position: "lsb" }, { number: 48, name: "controller48", description: "General Purpose Controller 1 (Fine)", position: "lsb" }, { number: 49, name: "controller49", description: "General Purpose Controller 2 (Fine)", position: "lsb" }, { number: 50, name: "controller50", description: "General Purpose Controller 3 (Fine)", position: "lsb" }, { number: 51, name: "controller51", description: "General Purpose Controller 4 (Fine)", position: "lsb" }, { number: 52, name: "controller52", description: "Undefined", position: "lsb" }, { number: 53, name: "controller53", description: "Undefined", position: "lsb" }, { number: 54, name: "controller54", description: "Undefined", position: "lsb" }, { number: 55, name: "controller55", description: "Undefined", position: "lsb" }, { number: 56, name: "controller56", description: "Undefined", position: "lsb" }, { number: 57, name: "controller57", description: "Undefined", position: "lsb" }, { number: 58, name: "controller58", description: "Undefined", position: "lsb" }, { number: 59, name: "controller59", description: "Undefined", position: "lsb" }, { number: 60, name: "controller60", description: "Undefined", position: "lsb" }, { number: 61, name: "controller61", description: "Undefined", position: "lsb" }, { number: 62, name: "controller62", description: "Undefined", position: "lsb" }, { number: 63, name: "controller63", description: "Undefined", position: "lsb" }, { number: 64, name: "damperpedal", description: "Damper Pedal On/Off" }, { number: 65, name: "portamento", description: "Portamento On/Off" }, { number: 66, name: "sostenuto", description: "Sostenuto On/Off" }, { number: 67, name: "softpedal", description: "Soft Pedal On/Off" }, { number: 68, name: "legatopedal", description: "Legato Pedal On/Off" }, { number: 69, name: "hold2", description: "Hold 2 On/Off" }, { number: 70, name: "soundvariation", description: "Sound Variation", position: "lsb" }, { number: 71, name: "resonance", description: "Resonance", position: "lsb" }, { number: 72, name: "releasetime", description: "Release Time", position: "lsb" }, { number: 73, name: "attacktime", description: "Attack Time", position: "lsb" }, { number: 74, name: "brightness", description: "Brightness", position: "lsb" }, { number: 75, name: "decaytime", description: "Decay Time", position: "lsb" }, { number: 76, name: "vibratorate", description: "Vibrato Rate", position: "lsb" }, { number: 77, name: "vibratodepth", description: "Vibrato Depth", position: "lsb" }, { number: 78, name: "vibratodelay", description: "Vibrato Delay", position: "lsb" }, { number: 79, name: "controller79", description: "Undefined", position: "lsb" }, { number: 80, name: "generalpurposecontroller5", description: "General Purpose Controller 5", position: "lsb" }, { number: 81, name: "generalpurposecontroller6", description: "General Purpose Controller 6", position: "lsb" }, { number: 82, name: "generalpurposecontroller7", description: "General Purpose Controller 7", position: "lsb" }, { number: 83, name: "generalpurposecontroller8", description: "General Purpose Controller 8", position: "lsb" }, { number: 84, name: "portamentocontrol", description: "Portamento Control", position: "lsb" }, { number: 85, name: "controller85", description: "Undefined" }, { number: 86, name: "controller86", description: "Undefined" }, { number: 87, name: "controller87", description: "Undefined" }, { number: 88, name: "highresolutionvelocityprefix", description: "High Resolution Velocity Prefix", position: "lsb" }, { number: 89, name: "controller89", description: "Undefined" }, { number: 90, name: "controller90", description: "Undefined" }, { number: 91, name: "effect1depth", description: "Effects 1 Depth (Reverb Send Level)" }, { number: 92, name: "effect2depth", description: "Effects 2 Depth" }, { number: 93, name: "effect3depth", description: "Effects 3 Depth (Chorus Send Level)" }, { number: 94, name: "effect4depth", description: "Effects 4 Depth" }, { number: 95, name: "effect5depth", description: "Effects 5 Depth" }, { number: 96, name: "dataincrement", description: "Data Increment" }, { number: 97, name: "datadecrement", description: "Data Decrement" }, { number: 98, name: "nonregisteredparameterfine", description: "Non-Registered Parameter Number (Fine)", position: "lsb" }, { number: 99, name: "nonregisteredparametercoarse", description: "Non-Registered Parameter Number (Coarse)", position: "msb" }, { number: 100, name: "registeredparameterfine", description: "Registered Parameter Number (Fine)", position: "lsb" }, { number: 101, name: "registeredparametercoarse", description: "Registered Parameter Number (Coarse)", position: "msb" }, { number: 102, name: "controller102", description: "Undefined" }, { number: 103, name: "controller103", description: "Undefined" }, { number: 104, name: "controller104", description: "Undefined" }, { number: 105, name: "controller105", description: "Undefined" }, { number: 106, name: "controller106", description: "Undefined" }, { number: 107, name: "controller107", description: "Undefined" }, { number: 108, name: "controller108", description: "Undefined" }, { number: 109, name: "controller109", description: "Undefined" }, { number: 110, name: "controller110", description: "Undefined" }, { number: 111, name: "controller111", description: "Undefined" }, { number: 112, name: "controller112", description: "Undefined" }, { number: 113, name: "controller113", description: "Undefined" }, { number: 114, name: "controller114", description: "Undefined" }, { number: 115, name: "controller115", description: "Undefined" }, { number: 116, name: "controller116", description: "Undefined" }, { number: 117, name: "controller117", description: "Undefined" }, { number: 118, name: "controller118", description: "Undefined" }, { number: 119, name: "controller119", description: "Undefined" }, { number: 120, name: "allsoundoff", description: "All Sound Off" }, { number: 121, name: "resetallcontrollers", description: "Reset All Controllers" }, { number: 122, name: "localcontrol", description: "Local Control On/Off" }, { number: 123, name: "allnotesoff", description: "All Notes Off" }, { number: 124, name: "omnimodeoff", description: "Omni Mode Off" }, { number: 125, name: "omnimodeon", description: "Omni Mode On" }, { number: 126, name: "monomodeon", description: "Mono Mode On" }, { number: 127, name: "polymodeon", description: "Poly Mode On" }, ]; } /** * Enumeration of all MIDI registered parameters and their associated pair of numerical values. * MIDI registered parameters extend the original list of control change messages. Currently, * there are only a limited number of them: * * * | Control Function | [LSB, MSB] | * |------------------------------|--------------| * | `pitchbendrange` | [0x00, 0x00] | * | `channelfinetuning` | [0x00, 0x01] | * | `channelcoarsetuning` | [0x00, 0x02] | * | `tuningprogram` | [0x00, 0x03] | * | `tuningbank` | [0x00, 0x04] | * | `modulationrange` | [0x00, 0x05] | * | `azimuthangle` | [0x3D, 0x00] | * | `elevationangle` | [0x3D, 0x01] | * | `gain` | [0x3D, 0x02] | * | `distanceratio` | [0x3D, 0x03] | * | `maximumdistance` | [0x3D, 0x04] | * | `maximumdistancegain` | [0x3D, 0x05] | * | `referencedistanceratio` | [0x3D, 0x06] | * | `panspreadangle` | [0x3D, 0x07] | * | `rollangle` | [0x3D, 0x08] | * * @enum {Object.} * @readonly * @since 3.1 * @static */ static get REGISTERED_PARAMETERS() { return { pitchbendrange: [0x00, 0x00], channelfinetuning: [0x00, 0x01], channelcoarsetuning: [0x00, 0x02], tuningprogram: [0x00, 0x03], tuningbank: [0x00, 0x04], modulationrange: [0x00, 0x05], azimuthangle: [0x3D, 0x00], elevationangle: [0x3D, 0x01], gain: [0x3D, 0x02], distanceratio: [0x3D, 0x03], maximumdistance: [0x3D, 0x04], maximumdistancegain: [0x3D, 0x05], referencedistanceratio: [0x3D, 0x06], panspreadangle: [0x3D, 0x07], rollangle: [0x3D, 0x08] }; } /** * @enum {Object.} * @readonly * @deprecated since 3.1 (use Enumerations.REGISTERED_PARAMETERS instead) * @private * @static */ static get MIDI_REGISTERED_PARAMETERS() { if (this.validation) { console.warn( "The MIDI_REGISTERED_PARAMETERS enum has been deprecated. Use the " + "Enumerations.REGISTERED_PARAMETERS enum instead." ); } return Enumerations.MIDI_REGISTERED_PARAMETERS; } /** * Enumeration of all valid MIDI system messages and matching numerical values. This library also * uses two additional custom messages. * * **System Common Messages** * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `sysex` | 0xF0 | 240 | * | `timecode` | 0xF1 | 241 | * | `songposition` | 0xF2 | 242 | * | `songselect` | 0xF3 | 243 | * | `tunerequest` | 0xF6 | 246 | * | `sysexend` | 0xF7 | 247 | * * The `sysexend` message is never actually received. It simply ends a sysex stream. * * **System Real-Time Messages** * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `clock` | 0xF8 | 248 | * | `start` | 0xFA | 250 | * | `continue` | 0xFB | 251 | * | `stop` | 0xFC | 252 | * | `activesensing` | 0xFE | 254 | * | `reset` | 0xFF | 255 | * * Values 249 and 253 are relayed by the * [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) but they do not * serve any specific purpose. The * [MIDI 1.0 spec](https://www.midi.org/specifications/item/table-1-summary-of-midi-message) * simply states that they are undefined/reserved. * * **Custom Messages** * * These two messages are mostly for internal use. They are not MIDI messages and cannot be sent * or forwarded. * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `midimessage` | | 0 | * | `unknownsystemmessage` | | -1 | * * @enum {Object.} * @readonly * @since 3.1 * @static */ static get SYSTEM_MESSAGES() { return { // System common messages sysex: 0xF0, // 240 timecode: 0xF1, // 241 songposition: 0xF2, // 242 songselect: 0xF3, // 243 tunerequest: 0xF6, // 246 tuningrequest: 0xF6, // for backwards-compatibility (deprecated in version 3.0) sysexend: 0xF7, // 247 (never actually received - simply ends a sysex) // System real-time messages clock: 0xF8, // 248 start: 0xFA, // 250 continue: 0xFB, // 251 stop: 0xFC, // 252 activesensing: 0xFE, // 254 reset: 0xFF, // 255 // Custom WebMidi.js messages midimessage: 0, unknownsystemmessage: -1 }; } /** * @enum {Object.} * @readonly * @deprecated since 3.1 (use Enumerations.SYSTEM_MESSAGES instead) * @private * @static */ static get MIDI_SYSTEM_MESSAGES() { if (this.validation) { console.warn( "The MIDI_SYSTEM_MESSAGES enum has been deprecated. Use the " + "Enumerations.SYSTEM_MESSAGES enum instead." ); } return Enumerations.SYSTEM_MESSAGES; } /** * Array of channel-specific event names that can be listened for. This includes channel mode * events and RPN/NRPN events. * * @type {string[]} * @readonly */ static get CHANNEL_EVENTS() { return [ // MIDI channel message events "noteoff", "controlchange", "noteon", "keyaftertouch", "programchange", "channelaftertouch", "pitchbend", // MIDI channel mode events "allnotesoff", "allsoundoff", "localcontrol", "monomode", "omnimode", "resetallcontrollers", // RPN/NRPN events "nrpn", "nrpn-dataentrycoarse", "nrpn-dataentryfine", "nrpn-dataincrement", "nrpn-datadecrement", "rpn", "rpn-dataentrycoarse", "rpn-dataentryfine", "rpn-dataincrement", "rpn-datadecrement", // Legacy (remove in v4) "nrpn-databuttonincrement", "nrpn-databuttondecrement", "rpn-databuttonincrement", "rpn-databuttondecrement", ]; } } ================================================ FILE: src/Forwarder.js ================================================ import {Enumerations} from "./Enumerations.js"; import {Output} from "./Output.js"; import {WebMidi} from "./WebMidi.js"; /** * The `Forwarder` class allows the forwarding of MIDI messages to predetermined outputs. When you * call its [`forward()`](#forward) method, it will send the specified [`Message`](Message) object * to all the outputs listed in its [`destinations`](#destinations) property. * * If specific channels or message types have been defined in the [`channels`](#channels) or * [`types`](#types) properties, only messages matching the channels/types will be forwarded. * * While it can be manually instantiated, you are more likely to come across a `Forwarder` object as * the return value of the [`Input.addForwarder()`](Input#addForwarder) method. * * @license Apache-2.0 * @since 3.0.0 */ export class Forwarder { /** * Creates a `Forwarder` object. * * @param {Output|Output[]} [destinations=\[\]] An [`Output`](Output) object, or an array of such * objects, to forward the message to. * * @param {object} [options={}] * @param {string|string[]} [options.types=(all messages)] A MIDI message type or an array of such * types (`"noteon"`, `"controlchange"`, etc.), that the specified message must match in order to * be forwarded. If this option is not specified, all types of messages will be forwarded. Valid * messages are the ones found in either * [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) * or [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * A MIDI channel number or an array of channel numbers that the message must match in order to be * forwarded. By default all MIDI channels are included (`1` to `16`). */ constructor(destinations = [], options = {}) { /** * An array of [`Output`](Output) objects to forward the message to. * @type {Output[]} */ this.destinations = []; /** * An array of message types (`"noteon"`, `"controlchange"`, etc.) that must be matched in order * for messages to be forwarded. By default, this array includes all * [`Enumerations.SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) and * [`Enumerations.CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @type {string[]} */ this.types = [ ...Object.keys(Enumerations.SYSTEM_MESSAGES), ...Object.keys(Enumerations.CHANNEL_MESSAGES) ]; /** * An array of MIDI channel numbers that the message must match in order to be forwarded. By * default, this array includes all MIDI channels (`1` to `16`). * @type {number[]} */ this.channels = Enumerations.MIDI_CHANNEL_NUMBERS; /** * Indicates whether message forwarding is currently suspended or not in this forwarder. * @type {boolean} */ this.suspended = false; // Make sure parameters are arrays if (!Array.isArray(destinations)) destinations = [destinations]; if (options.types && !Array.isArray(options.types)) options.types = [options.types]; if (options.channels && !Array.isArray(options.channels)) options.channels = [options.channels]; if (WebMidi.validation) { // Validate destinations destinations.forEach(destination => { if ( !(destination instanceof Output) ) { throw new TypeError("Destinations must be of type 'Output'."); } }); // Validate types if (options.types !== undefined) { options.types.forEach(type => { if ( ! Enumerations.SYSTEM_MESSAGES.hasOwnProperty(type) && ! Enumerations.CHANNEL_MESSAGES.hasOwnProperty(type) ) { throw new TypeError("Type must be a valid message type."); } }); } // Validate channels if (options.channels !== undefined) { options.channels.forEach(channel => { if (! Enumerations.MIDI_CHANNEL_NUMBERS.includes(channel) ) { throw new TypeError("MIDI channel must be between 1 and 16."); } }); } } this.destinations = destinations; if (options.types) this.types = options.types; if (options.channels) this.channels = options.channels; } /** * Sends the specified message to the forwarder's destination(s) if it matches the specified * type(s) and channel(s). * * @param {Message} message The [`Message`](Message) object to forward. */ forward(message) { // Abort if forwarding is currently suspended if (this.suspended) return; // Abort if this message type should not be forwarded if (!this.types.includes(message.type)) return; // Abort if this channel should not be forwarded if (message.channel && !this.channels.includes(message.channel)) return; // Forward this.destinations.forEach(destination => { if (WebMidi.validation && !(destination instanceof Output)) return; destination.send(message); }); } } ================================================ FILE: src/Input.js ================================================ import {Enumerations} from "./Enumerations.js"; import {EventEmitter} from "../node_modules/djipevents/src/djipevents.js"; import {Forwarder} from "./Forwarder.js"; import {InputChannel} from "./InputChannel.js"; import {Message} from "./Message.js"; import {Utilities} from "./Utilities.js"; import {WebMidi} from "./WebMidi.js"; /** * The `Input` class represents a single MIDI input port. This object is automatically instantiated * by the library according to the host's MIDI subsystem and does not need to be directly * instantiated. Instead, you can access all `Input` objects by referring to the * [`WebMidi.inputs`](WebMidi#inputs) array. You can also retrieve inputs by using methods such as * [`WebMidi.getInputByName()`](WebMidi#getInputByName) and * [`WebMidi.getInputById()`](WebMidi#getInputById). * * Note that a single MIDI device may expose several inputs and/or outputs. * * **Important**: the `Input` class does not directly fire channel-specific MIDI messages * (such as [`noteon`](InputChannel#event:noteon) or * [`controlchange`](InputChannel#event:controlchange), etc.). The [`InputChannel`](InputChannel) * object does that. However, you can still use the * [`Input.addListener()`](#addListener) method to listen to channel-specific events on multiple * [`InputChannel`](InputChannel) objects at once. * * @fires Input#opened * @fires Input#disconnected * @fires Input#closed * @fires Input#midimessage * * @fires Input#sysex * @fires Input#timecode * @fires Input#songposition * @fires Input#songselect * @fires Input#tunerequest * @fires Input#clock * @fires Input#start * @fires Input#continue * @fires Input#stop * @fires Input#activesensing * @fires Input#reset * * @fires Input#unknownmidimessage * * @extends EventEmitter * @license Apache-2.0 */ export class Input extends EventEmitter { /** * Creates an `Input` object. * * @param {MIDIInput} midiInput [`MIDIInput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput) * object as provided by the MIDI subsystem (Web MIDI API). */ constructor(midiInput) { super(); /** * Reference to the actual MIDIInput object * @private */ this._midiInput = midiInput; /** * @type {number} * @private */ this._octaveOffset = 0; /** * Array containing the 16 [`InputChannel`](InputChannel) objects available for this `Input`. The * channels are numbered 1 through 16. * * @type {InputChannel[]} */ this.channels = []; for (let i = 1; i <= 16; i++) this.channels[i] = new InputChannel(this, i); /** * @type {Forwarder[]} * @private */ this._forwarders = []; // Setup listeners this._midiInput.onstatechange = this._onStateChange.bind(this); this._midiInput.onmidimessage = this._onMidiMessage.bind(this); } /** * Destroys the `Input` by removing all listeners, emptying the [`channels`](#channels) array and * unlinking the MIDI subsystem. This is mostly for internal use. * * @returns {Promise} */ async destroy() { this.removeListener(); this.channels.forEach(ch => ch.destroy()); this.channels = []; this._forwarders = []; if (this._midiInput) { this._midiInput.onstatechange = null; this._midiInput.onmidimessage = null; } await this.close(); this._midiInput = null; } /** * Executed when a `"statechange"` event occurs. * * @param e * @private */ _onStateChange(e) { let event = { timestamp: WebMidi.time, target: this, port: this // for consistency }; if (e.port.connection === "open") { /** * Event emitted when the `Input` has been opened by calling the [`open()`]{@link #open} * method. * * @event Input#opened * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `opened` * @property {Input} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. */ event.type = "opened"; this.emit("opened", event); } else if (e.port.connection === "closed" && e.port.state === "connected") { /** * Event emitted when the `Input` has been closed by calling the * [`close()`]{@link #close} method. * * @event Input#closed * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `closed` * @property {Input} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. */ event.type = "closed"; this.emit("closed", event); } else if (e.port.connection === "closed" && e.port.state === "disconnected") { /** * Event emitted when the `Input` becomes unavailable. This event is typically fired * when the MIDI device is unplugged. * * @event Input#disconnected * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `disconnected` * @property {Input} port Object with properties describing the {@link Input} that was * disconnected. This is not the actual `Input` as it is no longer available. * @property {Input} target The object that dispatched the event. */ event.type = "disconnected"; event.port = { connection: e.port.connection, id: e.port.id, manufacturer: e.port.manufacturer, name: e.port.name, state: e.port.state, type: e.port.type }; this.emit("disconnected", event); } else if (e.port.connection === "pending" && e.port.state === "disconnected") { // I don't see the need to forward that... } else { console.warn("This statechange event was not caught: ", e.port.connection, e.port.state); } } /** * Executed when a `"midimessage"` event is received * @param e * @private */ _onMidiMessage(e) { // Create Message object from MIDI data const message = new Message(e.data); /** * Event emitted when any MIDI message is received on an `Input`. * * @event Input#midimessage * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `midimessage` * * @since 2.1 */ const event = { port: this, target: this, message: message, timestamp: e.timeStamp, type: "midimessage", data: message.data, // @deprecated (will be removed in v4) rawData: message.data, // @deprecated (will be removed in v4) statusByte: message.data[0], // @deprecated (will be removed in v4) dataBytes: message.dataBytes // @deprecated (will be removed in v4) }; this.emit("midimessage", event); // Messages are forwarded to InputChannel if they are channel messages or parsed locally for // system messages. if (message.isSystemMessage) { // system messages this._parseEvent(event); } else if (message.isChannelMessage) { // channel messages this.channels[message.channel]._processMidiMessageEvent(event); } // Forward message if forwarders have been defined this._forwarders.forEach(forwarder => forwarder.forward(message)); } /** * @private */ _parseEvent(e) { // Make a shallow copy of the incoming event so we can use it as the new event. const event = Object.assign({}, e); event.type = event.message.type || "unknownmidimessage"; // Add custom property for 'songselect' if (event.type === "songselect") { event.song = e.data[1] + 1; // deprecated event.value = e.data[1]; event.rawValue = event.value; } // Emit event this.emit(event.type, event); } /** * Opens the input for usage. This is usually unnecessary as the port is opened automatically when * WebMidi is enabled. * * @returns {Promise} The promise is fulfilled with the `Input` object. */ async open() { // Explicitly opens the port for usage. This is not mandatory. When the port is not explicitly // opened, it is implicitly opened (asynchronously) when assigning a listener to the // `onmidimessage` property of the `MIDIInput`. We do it explicitly so that 'connected' events // are dispatched immediately and that we are ready to listen. try { await this._midiInput.open(); } catch (err) { return Promise.reject(err); } return Promise.resolve(this); } /** * Closes the input. When an input is closed, it cannot be used to listen to MIDI messages until * the input is opened again by calling [`Input.open()`](Input#open). * * **Note**: if what you want to do is stop events from being dispatched, you should use * [`eventsSuspended`](#eventsSuspended) instead. * * @returns {Promise} The promise is fulfilled with the `Input` object */ async close() { // We close the port. This triggers a statechange event which, in turn, will emit the 'closed' // event. if (!this._midiInput) return Promise.resolve(this); try { await this._midiInput.close(); } catch (err) { return Promise.reject(err); } return Promise.resolve(this); } /** * @private * @deprecated since v3.0.0 (moved to 'Utilities' class) */ getChannelModeByNumber() { if (WebMidi.validation) { console.warn( "The 'getChannelModeByNumber()' method has been moved to the 'Utilities' class." ); } } /** * Adds an event listener that will trigger a function callback when the specified event is * dispatched. The event usually is **input-wide** but can also be **channel-specific**. * * Input-wide events do not target a specific MIDI channel so it makes sense to listen for them * at the `Input` level and not at the [`InputChannel`](InputChannel) level. Channel-specific * events target a specific channel. Usually, in this case, you would add the listener to the * [`InputChannel`](InputChannel) object. However, as a convenience, you can also listen to * channel-specific events directly on an `Input`. This allows you to react to a channel-specific * event no matter which channel it actually came through. * * When listening for an event, you simply need to specify the event name and the function to * execute: * * ```javascript * const listener = WebMidi.inputs[0].addListener("midimessage", e => { * console.log(e); * }); * ``` * * Calling the function with an input-wide event (such as * [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object * that was created. * * If you call the function with a channel-specific event (such as * [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all * [`Listener`](Listener) objects that were created (one for each channel): * * ```javascript * const listeners = WebMidi.inputs[0].addListener("noteon", someFunction); * ``` * * You can also specify which channels you want to add the listener to: * * ```javascript * const listeners = WebMidi.inputs[0].addListener("noteon", someFunction, {channels: [1, 2, 3]}); * ``` * * In this case, `listeners` is an array containing 3 [`Listener`](Listener) objects. The order of * the listeners in the array follows the order the channels were specified in. * * Note that, when adding channel-specific listeners, it is the [`InputChannel`](InputChannel) * instance that actually gets a listener added and not the `Input` instance. You can check that * by calling [`InputChannel.hasListener()`](InputChannel#hasListener()). * * There are 8 families of events you can listen to: * * 1. **MIDI System Common** Events (input-wide) * * * [`songposition`]{@link Input#event:songposition} * * [`songselect`]{@link Input#event:songselect} * * [`sysex`]{@link Input#event:sysex} * * [`timecode`]{@link Input#event:timecode} * * [`tunerequest`]{@link Input#event:tunerequest} * * 2. **MIDI System Real-Time** Events (input-wide) * * * [`clock`]{@link Input#event:clock} * * [`start`]{@link Input#event:start} * * [`continue`]{@link Input#event:continue} * * [`stop`]{@link Input#event:stop} * * [`activesensing`]{@link Input#event:activesensing} * * [`reset`]{@link Input#event:reset} * * 3. **State Change** Events (input-wide) * * * [`opened`]{@link Input#event:opened} * * [`closed`]{@link Input#event:closed} * * [`disconnected`]{@link Input#event:disconnected} * * 4. **Catch-All** Events (input-wide) * * * [`midimessage`]{@link Input#event:midimessage} * * [`unknownmidimessage`]{@link Input#event:unknownmidimessage} * * 5. **Channel Voice** Events (channel-specific) * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * 6. **Channel Mode** Events (channel-specific) * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * 7. **NRPN** Events (channel-specific) * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * 8. **RPN** Events (channel-specific) * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string | EventEmitter.ANY_EVENT} The type of the event. * * @param listener {function} A callback function to execute when the specified event is detected. * This function will receive an event parameter object. For details on this object's properties, * check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to * listen on. If no channel is specified, all channels will be used. This parameter is ignored for * input-wide events. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener|Listener[]} If the event is input-wide, a single [`Listener`](Listener) * object is returned. If the event is channel-specific, an array of all the * [`Listener`](Listener) objects is returned (one for each channel). */ addListener(event, listener, options = {}) { if (WebMidi.validation) { // Legacy compatibility if (typeof options === "function") { let channels = (listener != undefined) ? [].concat(listener) : undefined; // clone listener = options; options = {channels: channels}; } } // Check if the event is channel-specific or input-wide if (Enumerations.CHANNEL_EVENTS.includes(event)) { // If no channel defined, use all. if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; let listeners = []; Utilities.sanitizeChannels(options.channels).forEach(ch => { listeners.push(this.channels[ch].addListener(event, listener, options)); }); return listeners; } else { return super.addListener(event, listener, options); } } /** * Adds a one-time event listener that will trigger a function callback when the specified event * happens. The event can be **channel-bound** or **input-wide**. Channel-bound events are * dispatched by [`InputChannel`]{@link InputChannel} objects and are tied to a specific MIDI * channel while input-wide events are dispatched by the `Input` object itself and are not tied * to a specific channel. * * Calling the function with an input-wide event (such as * [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object * that was created. * * If you call the function with a channel-specific event (such as * [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all * [`Listener`](Listener) objects that were created (one for each channel): * * ```javascript * const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction); * ``` * * You can also specify which channels you want to add the listener to: * * ```javascript * const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction, {channels: [1, 2, 3]}); * ``` * * In this case, the `listeners` variable contains an array of 3 [`Listener`](Listener) objects. * * The code above will add a listener for the `"noteon"` event and call `someFunction` when the * event is triggered on MIDI channels `1`, `2` or `3`. * * Note that, when adding events to channels, it is the [`InputChannel`](InputChannel) instance * that actually gets a listener added and not the `Input` instance. * * Note: if you want to add a listener to a single MIDI channel you should probably do so directly * on the [`InputChannel`](InputChannel) object itself. * * There are 8 families of events you can listen to: * * 1. **MIDI System Common** Events (input-wide) * * * [`songposition`]{@link Input#event:songposition} * * [`songselect`]{@link Input#event:songselect} * * [`sysex`]{@link Input#event:sysex} * * [`timecode`]{@link Input#event:timecode} * * [`tunerequest`]{@link Input#event:tunerequest} * * 2. **MIDI System Real-Time** Events (input-wide) * * * [`clock`]{@link Input#event:clock} * * [`start`]{@link Input#event:start} * * [`continue`]{@link Input#event:continue} * * [`stop`]{@link Input#event:stop} * * [`activesensing`]{@link Input#event:activesensing} * * [`reset`]{@link Input#event:reset} * * 3. **State Change** Events (input-wide) * * * [`opened`]{@link Input#event:opened} * * [`closed`]{@link Input#event:closed} * * [`disconnected`]{@link Input#event:disconnected} * * 4. **Catch-All** Events (input-wide) * * * [`midimessage`]{@link Input#event:midimessage} * * [`unknownmidimessage`]{@link Input#event:unknownmidimessage} * * 5. **Channel Voice** Events (channel-specific) * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * 6. **Channel Mode** Events (channel-specific) * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * 7. **NRPN** Events (channel-specific) * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * 8. **RPN** Events (channel-specific) * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string} The type of the event. * * @param listener {function} A callback function to execute when the specified event is detected. * This function will receive an event parameter object. For details on this object's properties, * check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of * such integers representing the MIDI channel(s) to listen on. This parameter is ignored for * input-wide events. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @returns {Listener[]} An array of all [`Listener`](Listener) objects that were created. */ addOneTimeListener(event, listener, options = {}) { options.remaining = 1; return this.addListener(event, listener, options); } /** * This is an alias to the [Input.addListener()]{@link Input#addListener} method. * @since 2.0.0 * @deprecated since v3.0 * @private */ on(event, channel, listener, options) { return this.addListener(event, channel, listener, options); } /** * Checks if the specified event type is already defined to trigger the specified callback * function. For channel-specific events, the function will return `true` only if all channels * have the listener defined. * * @param event {string|Symbol} The type of the event. * * @param listener {function} The callback function to check for. * * @param {object} [options={}] * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of such * integers representing the MIDI channel(s) to check. This parameter is ignored for input-wide * events. * * @returns {boolean} Boolean value indicating whether or not the `Input` or * [`InputChannel`](InputChannel) already has this listener defined. */ hasListener(event, listener, options = {}) { if (WebMidi.validation) { // Legacy compatibility if (typeof options === "function") { let channels = [].concat(listener); // clone listener = options; options = {channels: channels}; } } if (Enumerations.CHANNEL_EVENTS.includes(event)) { // If no channel defined, use all. if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; return Utilities.sanitizeChannels(options.channels).every(ch => { return this.channels[ch].hasListener(event, listener); }); } else { return super.hasListener(event, listener); } } /** * Removes the specified event listener. If no listener is specified, all listeners matching the * specified event will be removed. If the event is channel-specific, the listener will be removed * from all [`InputChannel`]{@link InputChannel} objects belonging to that channel. If no event is * specified, all listeners for the `Input` as well as all listeners for all * [`InputChannel`]{@link InputChannel} objects belonging to the `Input` will be removed. * * By default, channel-specific listeners will be removed from all * [`InputChannel`]{@link InputChannel} objects unless the `options.channel` narrows it down. * * @param [type] {string} The type of the event. * * @param [listener] {function} The callback function to check for. * * @param {object} [options={}] * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of * such integers representing the MIDI channel(s) to match. This parameter is ignored for * input-wide events. * * @param {*} [options.context] Only remove the listeners that have this exact context. * * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener(event, listener, options = {}) { if (WebMidi.validation) { // Legacy compatibility if (typeof options === "function") { let channels = [].concat(listener); // clone listener = options; options = {channels: channels}; } } if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; // If the event is not specified, remove everything (channel-specific and input-wide)! if (event == undefined) { Utilities.sanitizeChannels(options.channels).forEach(ch => { if (this.channels[ch]) this.channels[ch].removeListener(); }); return super.removeListener(); } // If the event is specified, check if it's channel-specific or input-wide. if (Enumerations.CHANNEL_EVENTS.includes(event)) { Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].removeListener(event, listener, options); }); } else { super.removeListener(event, listener, options); } } /** * Adds a forwarder that will forward all incoming MIDI messages matching the criteria to the * specified [`Output`](Output) destination(s). This is akin to the hardware MIDI THRU port, with * the added benefit of being able to filter which data is forwarded. * * @param {Output|Output[]|Forwarder} output An [`Output`](Output) object, a * [`Forwarder`](Forwarder) object or an array of such objects, to forward messages to. * @param {object} [options={}] * @param {string|string[]} [options.types=(all messages)] A message type, or an array of such * types (`noteon`, `controlchange`, etc.), that the message type must match in order to be * forwarded. If this option is not specified, all types of messages will be forwarded. Valid * messages are the ones found in either * [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) or * [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * A MIDI channel number or an array of channel numbers that the message must match in order to be * forwarded. By default all MIDI channels are included (`1` to `16`). * * @returns {Forwarder} The [`Forwarder`](Forwarder) object created to handle the forwarding. This * is useful if you wish to manipulate or remove the [`Forwarder`](Forwarder) later on. */ addForwarder(output, options = {}) { let forwarder; // Unless 'output' is a forwarder, create a new forwarder if (output instanceof Forwarder) { forwarder = output; } else { forwarder = new Forwarder(output, options); } this._forwarders.push(forwarder); return forwarder; } /** * Removes the specified [`Forwarder`](Forwarder) object from the input. * * @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to remove (the * [`Forwarder`](Forwarder) object is returned when calling `addForwarder()`. */ removeForwarder(forwarder) { this._forwarders = this._forwarders.filter(item => item !== forwarder); } /** * Checks whether the specified [`Forwarder`](Forwarder) object has already been attached to this * input. * * @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to check for (the * [`Forwarder`](Forwarder) object is returned when calling [`addForwarder()`](#addForwarder). * @returns {boolean} */ hasForwarder(forwarder) { return this._forwarders.includes(forwarder); } /** * Name of the MIDI input. * * @type {string} * @readonly */ get name() { return this._midiInput.name; } /** * ID string of the MIDI port. The ID is host-specific. Do not expect the same ID on different * platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for * the same port. * * @type {string} * @readonly */ get id() { return this._midiInput.id; } /** * Input port's connection state: `pending`, `open` or `closed`. * * @type {string} * @readonly */ get connection() { return this._midiInput.connection; } /** * Name of the manufacturer of the device that makes this input port available. * * @type {string} * @readonly */ get manufacturer() { return this._midiInput.manufacturer; } /** * An integer to offset the reported octave of incoming notes. By default, middle C (MIDI note * number 60) is placed on the 4th octave (C4). * * If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If * `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. * * Note that this value is combined with the global offset value defined in the * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) property (if any). * * @type {number} * * @since 3.0 */ get octaveOffset() { return this._octaveOffset; } set octaveOffset(value) { if (this.validation) { value = parseInt(value); if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer."); } this._octaveOffset = value; } /** * State of the input port: `connected` or `disconnected`. * * @type {string} * @readonly */ get state() { return this._midiInput.state; } /** * The port type. In the case of the `Input` object, this is always: `input`. * * @type {string} * @readonly */ get type() { return this._midiInput.type; } /** * @type {boolean} * @private * @deprecated since v3.0.0 (moved to 'InputChannel' class) */ get nrpnEventsEnabled() { if (WebMidi.validation) { console.warn("The 'nrpnEventsEnabled' property has been moved to the 'InputChannel' class."); } return false; } } // Events that do not have code below them must be placed outside the class definition (?!) /** * Input-wide (system) event emitted when a **system exclusive** message has been received. * You should note that, to receive `sysex` events, you must call the * [`WebMidi.enable()`](WebMidi#enable()) method with the `sysex` option set to `true`: * * ```js * WebMidi.enable({sysex: true}) * .then(() => console.log("WebMidi has been enabled with sysex support.")) * ``` * * @event Input#sysex * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `sysex` * */ /** * Input-wide (system) event emitted when a **time code quarter frame** message has been * received. * * @event Input#timecode * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `timecode` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **song position** message has been received. * * @event Input#songposition * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `songposition` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **song select** message has been received. * * @event Input#songselect * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} value Song (or sequence) number to select (0-127) * @property {string} rawValue Song (or sequence) number to select (0-127) * * @since 2.1 */ /** * Input-wide (system) event emitted when a **tune request** message has been received. * * @event Input#tunerequest * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `tunerequest` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **timing clock** message has been received. * * @event Input#clock * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `clock` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **start** message has been received. * * @event Input#start * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `start` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **continue** message has been received. * * @event Input#continue * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `continue` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **stop** message has been received. * * @event Input#stop * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `stop` * * @since 2.1 */ /** * Input-wide (system) event emitted when an **active sensing** message has been received. * * @event Input#activesensing * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `activesensing` * * @since 2.1 */ /** * Input-wide (system) event emitted when a **reset** message has been received. * * @event Input#reset * * @type {object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `reset` * * @since 2.1 */ /** * Input-wide (system) event emitted when an unknown MIDI message has been received. It could * be, for example, one of the undefined/reserved messages. * * @event Input#unknownmessage * * @type {Object} * * @property {Input} port The `Input` that triggered the event. * @property {Input} target The object that dispatched the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `unknownmessage` * * @since 2.1 */ ================================================ FILE: src/InputChannel.js ================================================ import {EventEmitter} from "../node_modules/djipevents/src/djipevents.js"; import {WebMidi} from "./WebMidi.js"; import {Utilities} from "./Utilities.js"; import {Note} from "./Note.js"; import {Enumerations} from "./Enumerations.js"; /** * The `InputChannel` class represents a single MIDI input channel (1-16) from a single input * device. This object is derived from the host's MIDI subsystem and should not be instantiated * directly. * * All 16 `InputChannel` objects can be found inside the input's [`channels`](Input#channels) * property. * * @fires InputChannel#midimessage * @fires InputChannel#unknownmessage * * @fires InputChannel#noteoff * @fires InputChannel#noteon * @fires InputChannel#keyaftertouch * @fires InputChannel#programchange * @fires InputChannel#channelaftertouch * @fires InputChannel#pitchbend * * @fires InputChannel#allnotesoff * @fires InputChannel#allsoundoff * @fires InputChannel#localcontrol * @fires InputChannel#monomode * @fires InputChannel#omnimode * @fires InputChannel#resetallcontrollers * * @fires InputChannel#event:nrpn * @fires InputChannel#event:nrpn-dataentrycoarse * @fires InputChannel#event:nrpn-dataentryfine * @fires InputChannel#event:nrpn-dataincrement * @fires InputChannel#event:nrpn-datadecrement * @fires InputChannel#event:rpn * @fires InputChannel#event:rpn-dataentrycoarse * @fires InputChannel#event:rpn-dataentryfine * @fires InputChannel#event:rpn-dataincrement * @fires InputChannel#event:rpn-datadecrement * * @fires InputChannel#controlchange * @fires InputChannel#event:controlchange-controllerxxx * @fires InputChannel#event:controlchange-bankselectcoarse * @fires InputChannel#event:controlchange-modulationwheelcoarse * @fires InputChannel#event:controlchange-breathcontrollercoarse * @fires InputChannel#event:controlchange-footcontrollercoarse * @fires InputChannel#event:controlchange-portamentotimecoarse * @fires InputChannel#event:controlchange-dataentrycoarse * @fires InputChannel#event:controlchange-volumecoarse * @fires InputChannel#event:controlchange-balancecoarse * @fires InputChannel#event:controlchange-pancoarse * @fires InputChannel#event:controlchange-expressioncoarse * @fires InputChannel#event:controlchange-effectcontrol1coarse * @fires InputChannel#event:controlchange-effectcontrol2coarse * @fires InputChannel#event:controlchange-generalpurposecontroller1 * @fires InputChannel#event:controlchange-generalpurposecontroller2 * @fires InputChannel#event:controlchange-generalpurposecontroller3 * @fires InputChannel#event:controlchange-generalpurposecontroller4 * @fires InputChannel#event:controlchange-bankselectfine * @fires InputChannel#event:controlchange-modulationwheelfine * @fires InputChannel#event:controlchange-breathcontrollerfine * @fires InputChannel#event:controlchange-footcontrollerfine * @fires InputChannel#event:controlchange-portamentotimefine * @fires InputChannel#event:controlchange-dataentryfine * @fires InputChannel#event:controlchange-channelvolumefine * @fires InputChannel#event:controlchange-balancefine * @fires InputChannel#event:controlchange-panfine * @fires InputChannel#event:controlchange-expressionfine * @fires InputChannel#event:controlchange-effectcontrol1fine * @fires InputChannel#event:controlchange-effectcontrol2fine * @fires InputChannel#event:controlchange-damperpedal * @fires InputChannel#event:controlchange-portamento * @fires InputChannel#event:controlchange-sostenuto * @fires InputChannel#event:controlchange-softpedal * @fires InputChannel#event:controlchange-legatopedal * @fires InputChannel#event:controlchange-hold2 * @fires InputChannel#event:controlchange-soundvariation * @fires InputChannel#event:controlchange-resonance * @fires InputChannel#event:controlchange-releasetime * @fires InputChannel#event:controlchange-attacktime * @fires InputChannel#event:controlchange-brightness * @fires InputChannel#event:controlchange-decaytime * @fires InputChannel#event:controlchange-vibratorate * @fires InputChannel#event:controlchange-vibratodepth * @fires InputChannel#event:controlchange-vibratodelay * @fires InputChannel#event:controlchange-generalpurposecontroller5 * @fires InputChannel#event:controlchange-generalpurposecontroller6 * @fires InputChannel#event:controlchange-generalpurposecontroller7 * @fires InputChannel#event:controlchange-generalpurposecontroller8 * @fires InputChannel#event:controlchange-portamentocontrol * @fires InputChannel#event:controlchange-highresolutionvelocityprefix * @fires InputChannel#event:controlchange-effect1depth * @fires InputChannel#event:controlchange-effect2depth * @fires InputChannel#event:controlchange-effect3depth * @fires InputChannel#event:controlchange-effect4depth * @fires InputChannel#event:controlchange-effect5depth * @fires InputChannel#event:controlchange-dataincrement * @fires InputChannel#event:controlchange-datadecrement * @fires InputChannel#event:controlchange-nonregisteredparameterfine * @fires InputChannel#event:controlchange-nonregisteredparametercoarse * @fires InputChannel#event:controlchange-registeredparameterfine * @fires InputChannel#event:controlchange-registeredparametercoarse * @fires InputChannel#event:controlchange-allsoundoff * @fires InputChannel#event:controlchange-resetallcontrollers * @fires InputChannel#event:controlchange-localcontrol * @fires InputChannel#event:controlchange-allnotesoff * @fires InputChannel#event:controlchange-omnimodeoff * @fires InputChannel#event:controlchange-omnimodeon * @fires InputChannel#event:controlchange-monomodeon * @fires InputChannel#event:controlchange-polymodeon * @fires InputChannel#event: * * @extends EventEmitter * @license Apache-2.0 * @since 3.0.0 */ export class InputChannel extends EventEmitter { /** * Creates an `InputChannel` object. * * @param {Input} input The [`Input`](Input) object this channel belongs to. * @param {number} number The channel's MIDI number (1-16). */ constructor(input, number) { super(); /** * @type {Input} * @private */ this._input = input; /** * @type {number} * @private */ this._number = number; /** * @type {number} * @private */ this._octaveOffset = 0; /** * An array of messages that form the current NRPN sequence * @private * @type {Message[]} */ this._nrpnBuffer = []; /** * An array of messages that form the current RPN sequence * @private * @type {Message[]} */ this._rpnBuffer = []; /** * Indicates whether events for **Registered Parameter Number** and **Non-Registered Parameter * Number** should be dispatched. RPNs and NRPNs are composed of a sequence of specific * **control change** messages. When a valid sequence of such control change messages is * received, an [`rpn`](#event-rpn) or [`nrpn`](#event-nrpn) event will fire. * * If an invalid or out-of-order **control change** message is received, it will fall through * the collector logic and all buffered **control change** messages will be discarded as * incomplete. * * @type {boolean} */ this.parameterNumberEventsEnabled = true; /** * Contains the current playing state of all MIDI notes of this channel (0-127). The state is * `true` for a currently playing note and `false` otherwise. * @type {boolean[]} */ this.notesState = new Array(128).fill(false); } /** * Destroys the `InputChannel` by removing all listeners and severing the link with the MIDI * subsystem's input. */ destroy() { this._input = null; this._number = null; this._octaveOffset = 0; this._nrpnBuffer = []; this.notesState = new Array(128).fill(false); this.parameterNumberEventsEnabled = false; this.removeListener(); } /** * @param e MIDIMessageEvent * @private */ _processMidiMessageEvent(e) { // Create and emit a new 'midimessage' event based on the incoming one const event = Object.assign({}, e); event.port = this.input; event.target = this; event.type = "midimessage"; /** * Event emitted when a MIDI message of any kind is received by an `InputChannel` * * @event InputChannel#midimessage * * @type {object} * * @property {string} type `midimessage` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). */ this.emit(event.type, event); // Parse the inbound event for regular MIDI messages this._parseEventForStandardMessages(event); } /** * Parses incoming channel events and emit standard MIDI message events (noteon, noteoff, etc.) * @param e Event * @private */ _parseEventForStandardMessages(e) { const event = Object.assign({}, e); event.type = event.message.type || "unknownmessage"; const data1 = e.message.dataBytes[0]; const data2 = e.message.dataBytes[1]; if ( event.type === "noteoff" || (event.type === "noteon" && data2 === 0) ) { this.notesState[data1] = false; event.type = "noteoff"; // necessary for note on with 0 velocity /** * Event emitted when a **note off** MIDI message has been received on the channel. * * @event InputChannel#noteoff * * @type {object} * @property {string} type `noteoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the incoming * MIDI message. * @property {number} timestamp The moment * ([`DOMHighResTimeStamp`](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp)) * when the event occurred (in milliseconds since the navigation start of the document). * * @property {object} note A [`Note`](Note) object containing information such as note name, * octave and release velocity. * @property {number} value The release velocity amount expressed as a float between 0 and 1. * @property {number} rawValue The release velocity amount expressed as an integer (between 0 * and 127). */ // The object created when a noteoff event arrives is a Note with an attack velocity of 0. event.note = new Note( Utilities.offsetNumber( data1, this.octaveOffset + this.input.octaveOffset + WebMidi.octaveOffset ), { rawAttack: 0, rawRelease: data2, } ); event.value = Utilities.from7bitToFloat(data2); event.rawValue = data2; // Those are kept for backwards-compatibility but are gone from the documentation. They will // be removed in future versions (@deprecated). event.velocity = event.note.release; event.rawVelocity = event.note.rawRelease; } else if (event.type === "noteon") { this.notesState[data1] = true; /** * Event emitted when a **note on** MIDI message has been received. * * @event InputChannel#noteon * * @type {object} * @property {string} type `noteon` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} note A [`Note`](Note) object containing information such as note name, * octave and release velocity. * @property {number} value The attack velocity amount expressed as a float between 0 and 1. * @property {number} rawValue The attack velocity amount expressed as an integer (between 0 * and 127). */ event.note = new Note( Utilities.offsetNumber( data1, this.octaveOffset + this.input.octaveOffset + WebMidi.octaveOffset ), { rawAttack: data2 } ); event.value = Utilities.from7bitToFloat(data2); event.rawValue = data2; // Those are kept for backwards-compatibility but are gone from the documentation. They will // be removed in future versions (@deprecated). event.velocity = event.note.attack; event.rawVelocity = event.note.rawAttack; } else if (event.type === "keyaftertouch") { /** * Event emitted when a **key-specific aftertouch** MIDI message has been received. * * @event InputChannel#keyaftertouch * * @type {object} * @property {string} type `"keyaftertouch"` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} note A [`Note`](Note) object containing information such as note name * and number. * @property {number} value The aftertouch amount expressed as a float between 0 and 1. * @property {number} rawValue The aftertouch amount expressed as an integer (between 0 and * 127). */ event.note = new Note( Utilities.offsetNumber( data1, this.octaveOffset + this.input.octaveOffset + WebMidi.octaveOffset ) ); // Aftertouch value event.value = Utilities.from7bitToFloat(data2); event.rawValue = data2; // @deprecated event.identifier = event.note.identifier; event.key = event.note.number; event.rawKey = data1; } else if (event.type === "controlchange") { /** * Event emitted when a **control change** MIDI message has been received. * * @event InputChannel#controlchange * * @type {object} * @property {string} type `controlchange` * @property {string} subtype The type of control change message that was received. * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ event.controller = { number: data1, name: Enumerations.CONTROL_CHANGE_MESSAGES[data1].name, description: Enumerations.CONTROL_CHANGE_MESSAGES[data1].description, position: Enumerations.CONTROL_CHANGE_MESSAGES[data1].position, }; event.subtype = event.controller.name || "controller" + data1; event.value = Utilities.from7bitToFloat(data2); event.rawValue = data2; /** * Event emitted when a **control change** MIDI message has been received and that message is * targeting the controller numbered "xxx". Of course, "xxx" should be replaced by a valid * controller number (0-127). * * @event InputChannel#controlchange-controllerxxx * * @type {object} * @property {string} type `controlchange-controllerxxx` * @property {string} subtype The type of control change message that was received. * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ const numberedEvent = Object.assign({}, event); numberedEvent.type = `${event.type}-controller${data1}`; delete numberedEvent.subtype; this.emit(numberedEvent.type, numberedEvent); /** * Event emitted when a **controlchange-bankselectcoarse** MIDI message has been * received. * * @event InputChannel#controlchange-bankselectcoarse * * @type {object} * @property {string} type `controlchange-bankselectcoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-modulationwheelcoarse** MIDI message has been * received. * * @event InputChannel#controlchange-modulationwheelcoarse * * @type {object} * @property {string} type `controlchange-modulationwheelcoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-breathcontrollercoarse** MIDI message has been * received. * * @event InputChannel#controlchange-breathcontrollercoarse * * @type {object} * @property {string} type `controlchange-breathcontrollercoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-footcontrollercoarse** MIDI message has been * received. * * @event InputChannel#controlchange-footcontrollercoarse * * @type {object} * @property {string} type `controlchange-footcontrollercoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-portamentotimecoarse** MIDI message has been * received. * * @event InputChannel#controlchange-portamentotimecoarse * * @type {object} * @property {string} type `controlchange-portamentotimecoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-dataentrycoarse** MIDI message has been * received. * * @event InputChannel#controlchange-dataentrycoarse * * @type {object} * @property {string} type `controlchange-dataentrycoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-volumecoarse** MIDI message has been * received. * * @event InputChannel#controlchange-volumecoarse * * @type {object} * @property {string} type `controlchange-volumecoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-balancecoarse** MIDI message has been * received. * * @event InputChannel#controlchange-balancecoarse * * @type {object} * @property {string} type `controlchange-balancecoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-pancoarse** MIDI message has been * received. * * @event InputChannel#controlchange-pancoarse * * @type {object} * @property {string} type `controlchange-pancoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-expressioncoarse** MIDI message has been * received. * * @event InputChannel#controlchange-expressioncoarse * * @type {object} * @property {string} type `controlchange-expressioncoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effectcontrol1coarse** MIDI message has been * received. * * @event InputChannel#controlchange-effectcontrol1coarse * * @type {object} * @property {string} type `controlchange-effectcontrol1coarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effectcontrol2coarse** MIDI message has been * received. * * @event InputChannel#controlchange-effectcontrol2coarse * * @type {object} * @property {string} type `controlchange-effectcontrol2coarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller1** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller1 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller1` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller2** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller2 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller2` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller3** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller3 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller3` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller4** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller4 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller4` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-bankselectfine** MIDI message has been * received. * * @event InputChannel#controlchange-bankselectfine * * @type {object} * @property {string} type `controlchange-bankselectfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-modulationwheelfine** MIDI message has been * received. * * @event InputChannel#controlchange-modulationwheelfine * * @type {object} * @property {string} type `controlchange-modulationwheelfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-breathcontrollerfine** MIDI message has been * received. * * @event InputChannel#controlchange-breathcontrollerfine * * @type {object} * @property {string} type `controlchange-breathcontrollerfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-footcontrollerfine** MIDI message has been * received. * * @event InputChannel#controlchange-footcontrollerfine * * @type {object} * @property {string} type `controlchange-footcontrollerfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-portamentotimefine** MIDI message has been * received. * * @event InputChannel#controlchange-portamentotimefine * * @type {object} * @property {string} type `controlchange-portamentotimefine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-dataentryfine** MIDI message has been * received. * * @event InputChannel#controlchange-dataentryfine * * @type {object} * @property {string} type `controlchange-dataentryfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-channelvolumefine** MIDI message has been * received. * * @event InputChannel#controlchange-channelvolumefine * * @type {object} * @property {string} type `controlchange-channelvolumefine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-balancefine** MIDI message has been * received. * * @event InputChannel#controlchange-balancefine * * @type {object} * @property {string} type `controlchange-balancefine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-panfine** MIDI message has been * received. * * @event InputChannel#controlchange-panfine * * @type {object} * @property {string} type `controlchange-panfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-expressionfine** MIDI message has been * received. * * @event InputChannel#controlchange-expressionfine * * @type {object} * @property {string} type `controlchange-expressionfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effectcontrol1fine** MIDI message has been * received. * * @event InputChannel#controlchange-effectcontrol1fine * * @type {object} * @property {string} type `controlchange-effectcontrol1fine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effectcontrol2fine** MIDI message has been * received. * * @event InputChannel#controlchange-effectcontrol2fine * * @type {object} * @property {string} type `controlchange-effectcontrol2fine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-damperpedal** MIDI message has been * received. * * @event InputChannel#controlchange-damperpedal * * @type {object} * @property {string} type `controlchange-damperpedal` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-portamento** MIDI message has been * received. * * @event InputChannel#controlchange-portamento * * @type {object} * @property {string} type `controlchange-portamento` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-sostenuto** MIDI message has been * received. * * @event InputChannel#controlchange-sostenuto * * @type {object} * @property {string} type `controlchange-sostenuto` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-softpedal** MIDI message has been * received. * * @event InputChannel#controlchange-softpedal * * @type {object} * @property {string} type `controlchange-softpedal` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-legatopedal** MIDI message has been * received. * * @event InputChannel#controlchange-legatopedal * * @type {object} * @property {string} type `controlchange-legatopedal` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-hold2** MIDI message has been * received. * * @event InputChannel#controlchange-hold2 * * @type {object} * @property {string} type `controlchange-hold2` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-soundvariation** MIDI message has been * received. * * @event InputChannel#controlchange-soundvariation * * @type {object} * @property {string} type `controlchange-soundvariation` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-resonance** MIDI message has been * received. * * @event InputChannel#controlchange-resonance * * @type {object} * @property {string} type `controlchange-resonance` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-releasetime** MIDI message has been * received. * * @event InputChannel#controlchange-releasetime * * @type {object} * @property {string} type `controlchange-releasetime` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-attacktime** MIDI message has been * received. * * @event InputChannel#controlchange-attacktime * * @type {object} * @property {string} type `controlchange-attacktime` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-brightness** MIDI message has been * received. * * @event InputChannel#controlchange-brightness * * @type {object} * @property {string} type `controlchange-brightness` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-decaytime** MIDI message has been * received. * * @event InputChannel#controlchange-decaytime * * @type {object} * @property {string} type `controlchange-decaytime` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-vibratorate** MIDI message has been * received. * * @event InputChannel#controlchange-vibratorate * * @type {object} * @property {string} type `controlchange-vibratorate` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-vibratodepth** MIDI message has been * received. * * @event InputChannel#controlchange-vibratodepth * * @type {object} * @property {string} type `controlchange-vibratodepth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-vibratodelay** MIDI message has been * received. * * @event InputChannel#controlchange-vibratodelay * * @type {object} * @property {string} type `controlchange-vibratodelay` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller5** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller5 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller5` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller6** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller6 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller6` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller7** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller7 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller7` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-generalpurposecontroller8** MIDI message has been * received. * * @event InputChannel#controlchange-generalpurposecontroller8 * * @type {object} * @property {string} type `controlchange-generalpurposecontroller8` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-portamentocontrol** MIDI message has been * received. * * @event InputChannel#controlchange-portamentocontrol * * @type {object} * @property {string} type `controlchange-portamentocontrol` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-highresolutionvelocityprefix** MIDI message has been * received. * * @event InputChannel#controlchange-highresolutionvelocityprefix * * @type {object} * @property {string} type `controlchange-highresolutionvelocityprefix` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effect1depth** MIDI message has been * received. * * @event InputChannel#controlchange-effect1depth * * @type {object} * @property {string} type `controlchange-effect1depth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effect2depth** MIDI message has been * received. * * @event InputChannel#controlchange-effect2depth * * @type {object} * @property {string} type `controlchange-effect2depth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effect3depth** MIDI message has been * received. * * @event InputChannel#controlchange-effect3depth * * @type {object} * @property {string} type `controlchange-effect3depth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effect4depth** MIDI message has been * received. * * @event InputChannel#controlchange-effect4depth * * @type {object} * @property {string} type `controlchange-effect4depth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-effect5depth** MIDI message has been * received. * * @event InputChannel#controlchange-effect5depth * * @type {object} * @property {string} type `controlchange-effect5depth` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-dataincrement** MIDI message has been * received. * * @event InputChannel#controlchange-dataincrement * * @type {object} * @property {string} type `controlchange-dataincrement` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-datadecrement** MIDI message has been * received. * * @event InputChannel#controlchange-datadecrement * * @type {object} * @property {string} type `controlchange-datadecrement` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-nonregisteredparameterfine** MIDI message has been * received. * * @event InputChannel#controlchange-nonregisteredparameterfine * * @type {object} * @property {string} type `controlchange-nonregisteredparameterfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-nonregisteredparametercoarse** MIDI message has been * received. * * @event InputChannel#controlchange-nonregisteredparametercoarse * * @type {object} * @property {string} type `controlchange-nonregisteredparametercoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-registeredparameterfine** MIDI message has been * received. * * @event InputChannel#controlchange-registeredparameterfine * * @type {object} * @property {string} type `controlchange-registeredparameterfine` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-registeredparametercoarse** MIDI message has been * received. * * @event InputChannel#controlchange-registeredparametercoarse * * @type {object} * @property {string} type `controlchange-registeredparametercoarse` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-allsoundoff** MIDI message has been * received. * * @event InputChannel#controlchange-allsoundoff * * @type {object} * @property {string} type `controlchange-allsoundoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-resetallcontrollers** MIDI message has been * received. * * @event InputChannel#controlchange-resetallcontrollers * * @type {object} * @property {string} type `controlchange-resetallcontrollers` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-localcontrol** MIDI message has been * received. * * @event InputChannel#controlchange-localcontrol * * @type {object} * @property {string} type `controlchange-localcontrol` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-allnotesoff** MIDI message has been * received. * * @event InputChannel#controlchange-allnotesoff * * @type {object} * @property {string} type `controlchange-allnotesoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-omnimodeoff** MIDI message has been * received. * * @event InputChannel#controlchange-omnimodeoff * * @type {object} * @property {string} type `controlchange-omnimodeoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-omnimodeon** MIDI message has been * received. * * @event InputChannel#controlchange-omnimodeon * * @type {object} * @property {string} type `controlchange-omnimodeon` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-monomodeon** MIDI message has been * received. * * @event InputChannel#controlchange-monomodeon * * @type {object} * @property {string} type `controlchange-monomodeon` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ /** * Event emitted when a **controlchange-polymodeon** MIDI message has been * received. * * @event InputChannel#controlchange-polymodeon * * @type {object} * @property {string} type `controlchange-polymodeon` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {object} controller * @property {object} controller.number The number of the controller. * @property {object} controller.name The usual name or function of the controller. * @property {object} controller.description A user-friendly representation of the * controller's default function * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The value expressed as an integer (between 0 and 127). */ const namedEvent = Object.assign({}, event); namedEvent.type = `${event.type}-` + Enumerations.CONTROL_CHANGE_MESSAGES[data1].name; delete namedEvent.subtype; // Dispatch controlchange-"function" events only if the "function" is defined (not the generic // controllerXXX nomenclature) if (namedEvent.type.indexOf("controller") !== 0) { this.emit(namedEvent.type, namedEvent); } // Trigger channel mode message events (if appropriate) if (event.message.dataBytes[0] >= 120) this._parseChannelModeMessage(event); // Parse the inbound event to see if its part of an RPN/NRPN sequence if ( this.parameterNumberEventsEnabled && this._isRpnOrNrpnController(event.message.dataBytes[0]) ) { this._parseEventForParameterNumber(event); } } else if (event.type === "programchange") { /** * Event emitted when a **program change** MIDI message has been received. * * @event InputChannel#programchange * * @type {object} * @property {string} type `programchange` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {number} value The value expressed as an integer between 0 and 127. * @property {number} rawValue The raw MIDI value expressed as an integer between 0 and 127. */ event.value = data1; event.rawValue = event.value; } else if (event.type === "channelaftertouch") { /** * Event emitted when a control change MIDI message has been received. * * @event InputChannel#channelaftertouch * * @type {object} * @property {string} type `channelaftertouch` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The raw MIDI value expressed as an integer between 0 and 127. */ event.value = Utilities.from7bitToFloat(data1); event.rawValue = data1; } else if (event.type === "pitchbend") { /** * Event emitted when a pitch bend MIDI message has been received. * * @event InputChannel#pitchbend * * @type {object} * @property {string} type `pitchbend` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {number} value The value expressed as a float between 0 and 1. * @property {number} rawValue The raw MIDI value expressed as an integer (between 0 and * 16383). */ event.value = ((data2 << 7) + data1 - 8192) / 8192; event.rawValue = (data2 << 7) + data1; } else { event.type = "unknownmessage"; } this.emit(event.type, event); } /** * @param e {Object} * @private */ _parseChannelModeMessage(e) { // Make a shallow copy of the incoming event so we can use it as the new event. const event = Object.assign({}, e); event.type = event.controller.name; /** * Event emitted when an "all sound off" channel-mode MIDI message has been received. * * @event InputChannel#allsoundoff * * @type {object} * @property {string} type `allsoundoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). */ /** * Event emitted when a "reset all controllers" channel-mode MIDI message has been received. * * @event InputChannel#resetallcontrollers * * @type {object} * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). */ /** * Event emitted when a "local control" channel-mode MIDI message has been received. The value * property of the event is set to either `true` (local control on) of `false` (local control * off). * * @event InputChannel#localcontrol * * @type {object} * @property {string} type `localcontrol` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {boolean} value For local control on, the value is `true`. For local control off, * the value is `false`. * @property {boolean} rawValue For local control on, the value is `127`. For local control off, * the value is `0`. */ if (event.type === "localcontrol") { event.value = event.message.data[2] === 127 ? true : false; event.rawValue = event.message.data[2]; } /** * Event emitted when an "all notes off" channel-mode MIDI message has been received. * * @event InputChannel#allnotesoff * * @type {object} * @property {string} type `allnotesoff` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). */ /** * Event emitted when an "omni mode" channel-mode MIDI message has been received. The value * property of the event is set to either `true` (omni mode on) of `false` (omni mode off). * * @event InputChannel#omnimode * * @type {object} * @property {string} type `"omnimode"` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {boolean} value The value is `true` for omni mode on and false for omni mode off. * @property {boolean} rawValue The raw MIDI value */ if (event.type === "omnimodeon") { event.type = "omnimode"; event.value = true; event.rawValue = event.message.data[2]; } else if (event.type === "omnimodeoff") { event.type = "omnimode"; event.value = false; event.rawValue = event.message.data[2]; } /** * Event emitted when a "mono/poly mode" MIDI message has been received. The value property of * the event is set to either `true` (mono mode on / poly mode off) or `false` (mono mode off / * poly mode on). * * @event InputChannel#monomode * * @type {object} * @property {string} type `monomode` * * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * * @property {boolean} value The value is `true` for omni mode on and false for omni mode off. * @property {boolean} rawValue The raw MIDI value */ if (event.type === "monomodeon") { event.type = "monomode"; event.value = true; event.rawValue = event.message.data[2]; } else if (event.type === "polymodeon") { event.type = "monomode"; event.value = false; event.rawValue = event.message.data[2]; } this.emit(event.type, event); } /** * Parses inbound events to identify RPN/NRPN sequences. * @param e Event * @private */ _parseEventForParameterNumber(event) { // To make it more legible const controller = event.message.dataBytes[0]; const value = event.message.dataBytes[1]; // A. Check if the message is the start of an RPN (101) or NRPN (99) parameter declaration. if (controller === 99 || controller === 101) { this._nrpnBuffer = []; this._rpnBuffer = []; if (controller === 99) { // 99 this._nrpnBuffer = [event.message]; } else { // 101 // 127 is a reset so we ignore it if (value !== 127) this._rpnBuffer = [event.message]; } // B. Check if the message is the end of an RPN (100) or NRPN (98) parameter declaration. } else if (controller === 98 || controller === 100) { if (controller === 98) { // 98 // Flush the other buffer (they are mutually exclusive) this._rpnBuffer = []; // Check if we are in sequence if (this._nrpnBuffer.length === 1) { this._nrpnBuffer.push(event.message); } else { this._nrpnBuffer = []; // out of sequence } } else { // 100 // Flush the other buffer (they are mutually exclusive) this._nrpnBuffer = []; // 127 is a reset so we ignore it if (this._rpnBuffer.length === 1 && value !== 127) { this._rpnBuffer.push(event.message); } else { this._rpnBuffer = []; // out of sequence or reset } } // C. Check if the message is for data entry (6, 38, 96 or 97). Those messages trigger events. } else if ( controller === 6 || controller === 38 || controller === 96 || controller === 97 ) { if (this._rpnBuffer.length === 2) { this._dispatchParameterNumberEvent( "rpn", this._rpnBuffer[0].dataBytes[1], this._rpnBuffer[1].dataBytes[1], event ); } else if (this._nrpnBuffer.length === 2) { this._dispatchParameterNumberEvent( "nrpn", this._nrpnBuffer[0].dataBytes[1], this._nrpnBuffer[1].dataBytes[1], event ); } else { this._nrpnBuffer = []; this._rpnBuffer = []; } } } /** * Indicates whether the specified controller can be part of an RPN or NRPN sequence * @param controller * @returns {boolean} * @private */ _isRpnOrNrpnController(controller) { return controller === 6 || controller === 38 || controller === 96 || controller === 97 || controller === 98 || controller === 99 || controller === 100 || controller === 101; } /** * @private */ _dispatchParameterNumberEvent(type, paramMsb, paramLsb, e) { type = type === "nrpn" ? "nrpn" : "rpn"; /** * Event emitted when an **RPN data entry coarse** message is received on the input. The * specific parameter to which the message applies can be found in the event's `parameter` * property. It is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#rpn-dataentrycoarse * * @type {object} * * @property {string} type `rpn-dataentrycoarse` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **RPN data entry fine** message is received on the input. The * specific parameter to which the message applies can be found in the event's `parameter` * property. It is one of the ones defined in * [`EnumerationsREGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#rpn-dataentryfine * * @type {object} * * @property {string} type `rpn-dataentryfine` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **RPN data increment** message is received on the input. The specific * parameter to which the message applies can be found in the event's `parameter` property. It * is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#rpn-dataincrement * * @type {object} * * @property {string} type `rpn-dataincrement` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **RPN data decrement** message is received on the input. The specific * parameter to which the message applies can be found in the event's `parameter` property. It * is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#rpn-datadecrement * * @type {object} * * @property {string} type `rpn-datadecrement` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **NRPN data entry coarse** message is received on the input. The * specific parameter to which the message applies can be found in the event's `parameter` * property. It is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#nrpn-dataentrycoarse * * @type {object} * * @property {string} type `nrpn-dataentrycoarse` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **NRPN data entry fine** message is received on the input. The * specific parameter to which the message applies can be found in the event's `parameter` * property. It is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#nrpn-dataentryfine * * @type {object} * * @property {string} type `nrpn-dataentryfine` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **NRPN data increment** message is received on the input. The specific * parameter to which the message applies can be found in the event's `parameter` property. It * is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#nrpn-dataincrement * * @type {object} * * @property {string} type `nrpn-dataincrement` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when an **NRPN data decrement** message is received on the input. The specific * parameter to which the message applies can be found in the event's `parameter` property. It * is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#nrpn-datadecrement * * @type {object} * * @property {string} type `nrpn-datadecrement` * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ const event = { target: e.target, timestamp: e.timestamp, message: e.message, parameterMsb: paramMsb, parameterLsb: paramLsb, value: Utilities.from7bitToFloat(e.message.dataBytes[1]), rawValue: e.message.dataBytes[1], }; // Identify the parameter (by name for RPN and by number for NRPN) if (type === "rpn") { event.parameter = Object.keys(Enumerations.REGISTERED_PARAMETERS).find(key => { return Enumerations.REGISTERED_PARAMETERS[key][0] === paramMsb && Enumerations.REGISTERED_PARAMETERS[key][1] === paramLsb; }); } else { event.parameter = (paramMsb << 7) + paramLsb; } // Type and subtype const subtype = Enumerations.CONTROL_CHANGE_MESSAGES[e.message.dataBytes[0]].name; // Emit specific event event.type = `${type}-${subtype}`; this.emit(event.type, event); // Begin Legacy Block (remove in v4) const legacyEvent = Object.assign({}, event); if (legacyEvent.type === "nrpn-dataincrement") { legacyEvent.type = "nrpn-databuttonincrement"; } else if (legacyEvent.type === "nrpn-datadecrement") { legacyEvent.type = "nrpn-databuttondecrement"; } else if (legacyEvent.type === "rpn-dataincrement") { legacyEvent.type = "rpn-databuttonincrement"; } else if (legacyEvent.type === "rpn-datadecrement") { legacyEvent.type = "rpn-databuttondecrement"; } this.emit(legacyEvent.type, legacyEvent); // End Legacy Block /** * Event emitted when any NRPN message is received on the input. There are four subtypes of NRPN * messages: * * * `nrpn-dataentrycoarse` * * `nrpn-dataentryfine` * * `nrpn-dataincrement` * * `nrpn-datadecrement` * * The parameter to which the message applies can be found in the event's `parameter` property. * * @event InputChannel#nrpn * * @type {object} * * @property {string} type `nrpn` * @property {string} subtype The precise type of NRPN message that was received. * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {number} parameter The non-registered parameter number (0-16383) * @property {number} parameterMsb The MSB portion of the non-registered parameter number * (0-127) * @property {number} parameterLsb: The LSB portion of the non-registered parameter number * (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ /** * Event emitted when any RPN message is received on the input. There are four subtypes of RPN * messages: * * * `rpn-dataentrycoarse` * * `rpn-dataentryfine` * * `rpn-dataincrement` * * `rpn-datadecrement` * * The parameter to which the message applies can be found in the event's `parameter` property. * It is one of the ones defined in * [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). * * @event InputChannel#rpn * * @type {object} * * @property {string} type `rpn` * @property {string} subtype The precise type of RPN message that was received. * @property {InputChannel} target The object that dispatched the event. * @property {Input} port The `Input` that triggered the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {Message} message A [`Message`](Message) object containing information about the * incoming MIDI message. * @property {string} parameter The registered parameter's name * @property {number} parameterMsb The MSB portion of the registered parameter (0-127) * @property {number} parameterLsb: The LSB portion of the registered parameter (0-127) * @property {number} value The received value as a normalized number between 0 and 1. * @property {number} rawValue The value as received (0-127) */ // Emit general event event.type = type; event.subtype = subtype; this.emit(event.type, event); } /** * @deprecated since version 3. * @private */ getChannelModeByNumber(number) { if (WebMidi.validation) { console.warn( "The 'getChannelModeByNumber()' method has been moved to the 'Utilities' class." ); number = Math.floor(number); } return Utilities.getChannelModeByNumber(number); } /** * @deprecated since version 3. * @private */ getCcNameByNumber(number) { if (WebMidi.validation) { console.warn( "The 'getCcNameByNumber()' method has been moved to the 'Utilities' class." ); number = parseInt(number); if ( !(number >= 0 && number <= 127) ) throw new RangeError("Invalid control change number."); } return Utilities.getCcNameByNumber(number); } /** * Returns the playing status of the specified note (`true` if the note is currently playing, * `false` if it is not). The `note` parameter can be an unsigned integer (0-127), a note * identifier (`"C4"`, `"G#5"`, etc.) or a [`Note`]{@link Note} object. * * IF the note is specified using an integer (0-127), no octave offset will be applied. * * @param {number|string|Note} note The note to get the state for. The * [`octaveOffset`](#octaveOffset) (channel, input and global) will be factored in for note * identifiers and [`Note`]{@link Note} objects. * @returns {boolean} * @since version 3.0.0 */ getNoteState(note) { // If it's a note object, we simply use the identifier if (note instanceof Note) note = note.identifier; const number = Utilities.guessNoteNumber( note, WebMidi.octaveOffset + this.input.octaveOffset + this.octaveOffset ); return this.notesState[number]; } /** * An integer to offset the reported octave of incoming note-specific messages (`noteon`, * `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th * octave (C4). * * If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If * `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. * * Note that this value is combined with the global offset value defined by * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) object and with the value defined on the parent * input object with [`Input.octaveOffset`](Input#octaveOffset). * * @type {number} * * @since 3.0 */ get octaveOffset() { return this._octaveOffset; } set octaveOffset(value) { if (this.validation) { value = parseInt(value); if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer."); } this._octaveOffset = value; } /** * The [`Input`](Input) this channel belongs to. * @type {Input} * @since 3.0 */ get input() { return this._input; } /** * This channel's MIDI number (1-16). * @type {number} * @since 3.0 */ get number() { return this._number; } /** * Whether RPN/NRPN events are parsed and dispatched. * @type {boolean} * @since 3.0 * @deprecated Use parameterNumberEventsEnabled instead. * @private */ get nrpnEventsEnabled() { return this.parameterNumberEventsEnabled; } set nrpnEventsEnabled(value) { if (this.validation) { value = !!value; } this.parameterNumberEventsEnabled = value; } } ================================================ FILE: src/Message.js ================================================ import {Utilities} from "./Utilities.js"; import {Enumerations} from "./Enumerations.js"; /** * The `Message` class represents a single MIDI message. It has several properties that make it * easy to make sense of the binary data it contains. * * @license Apache-2.0 * @since 3.0.0 */ export class Message { /** * Creates a new `Message` object from raw MIDI data. * * @param {Uint8Array} data The raw data of the MIDI message as a * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * of integers between `0` and `255`. */ constructor(data) { /** * A * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * containing the bytes of the MIDI message. Each byte is an integer between `0` and `255`. * * @type {Uint8Array} * @readonly */ this.rawData = data; /** * An array containing all the bytes of the MIDI message. Each byte is an integer between `0` * and `255`. * * @type {number[]} * @readonly */ this.data = Array.from(this.rawData); /** * The MIDI status byte of the message as an integer between `0` and `255`. * * @type {number} * @readonly */ this.statusByte = this.rawData[0]; /** * A * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * of the data byte(s) of the MIDI message. When the message is a system exclusive message * (sysex), `rawDataBytes` explicitly excludes the manufacturer ID and the sysex end byte so * only the actual data is included. * * @type {Uint8Array} * @readonly */ this.rawDataBytes = this.rawData.slice(1); /** * An array of the the data byte(s) of the MIDI message (as opposed to the status byte). When * the message is a system exclusive message (sysex), `dataBytes` explicitly excludes the * manufacturer ID and the sysex end byte so only the actual data is included. * * @type {number[]} * @readonly */ this.dataBytes = this.data.slice(1); /** * A boolean indicating whether the MIDI message is a channel-specific message. * * @type {boolean} * @readonly */ this.isChannelMessage = false; /** * A boolean indicating whether the MIDI message is a system message (not specific to a * channel). * * @type {boolean} * @readonly */ this.isSystemMessage = false; /** * An integer identifying the MIDI command. For channel-specific messages, the value is 4-bit * and will be between `8` and `14`. For system messages, the value will be between `240` and * `255`. * * @type {number} * @readonly */ this.command = undefined; /** * The MIDI channel number (`1` - `16`) that the message is targeting. This is only for * channel-specific messages. For system messages, this will be left `undefined`. * * @type {number} * @readonly */ this.channel = undefined; /** * When the message is a system exclusive message (sysex), this property contains an array with * either 1 or 3 entries that identify the manufacturer targeted by the message. * * To know how to translate these entries into manufacturer names, check out the official list: * https://www.midi.org/specifications-old/item/manufacturer-id-numbers * * @type {number[]} * @readonly */ this.manufacturerId = undefined; /** * The type of message as a string (`"noteon"`, `"controlchange"`, `"sysex"`, etc.) * * @type {string} * @readonly */ this.type = undefined; // Assign values to property that vary according to whether they are channel-specific or system if (this.statusByte < 240) { this.isChannelMessage = true; this.command = this.statusByte >> 4; this.channel = (this.statusByte & 0b00001111) + 1; } else { this.isSystemMessage = true; this.command = this.statusByte; } // Assign type (depending on whether the message is channel-specific or system) if (this.isChannelMessage) { this.type = Utilities.getPropertyByValue(Enumerations.CHANNEL_MESSAGES, this.command); } else if (this.isSystemMessage) { this.type = Utilities.getPropertyByValue(Enumerations.SYSTEM_MESSAGES, this.command); } // When the message is a sysex message, we add a manufacturer property and strip out the id from // dataBytes and rawDataBytes. if (this.statusByte === Enumerations.SYSTEM_MESSAGES.sysex) { if (this.dataBytes[0] === 0) { this.manufacturerId = this.dataBytes.slice(0, 3); this.dataBytes = this.dataBytes.slice(3, this.rawDataBytes.length - 1); this.rawDataBytes = this.rawDataBytes.slice(3, this.rawDataBytes.length - 1); } else { this.manufacturerId = [this.dataBytes[0]]; this.dataBytes = this.dataBytes.slice(1, this.dataBytes.length - 1); this.rawDataBytes = this.rawDataBytes.slice(1, this.rawDataBytes.length - 1); } } } } ================================================ FILE: src/Note.js ================================================ import {WebMidi} from "./WebMidi.js"; import {Utilities} from "./Utilities.js"; /** * The `Note` class represents a single musical note such as `"D3"`, `"G#4"`, `"F-1"`, `"Gb7"`, etc. * * `Note` objects can be played back on a single channel by calling * [`OutputChannel.playNote()`]{@link OutputChannel#playNote} or, on multiple channels of the same * output, by calling [`Output.playNote()`]{@link Output#playNote}. * * The note has [`attack`](#attack) and [`release`](#release) velocities set at `0.5` by default. * These can be changed by passing in the appropriate option. It is also possible to set a * system-wide default for attack and release velocities by using the * [`WebMidi.defaults`](WebMidi#defaults) property. * * If you prefer to work with raw MIDI values (`0` to `127`), you can use [`rawAttack`](#rawAttack) and * [`rawRelease`](#rawRelease) to both get and set the values. * * The note may have a [`duration`](#duration). If it does, playback will be automatically stopped * when the duration has elapsed by sending a `"noteoff"` event. By default, the duration is set to * `Infinity`. In this case, it will never stop playing unless explicitly stopped by calling a * method such as [`OutputChannel.stopNote()`]{@link OutputChannel#stopNote}, * [`Output.stopNote()`]{@link Output#stopNote} or similar. * * @license Apache-2.0 * @since 3.0.0 */ export class Note { /** * Creates a `Note` object. * * @param value {string|number} The value used to create the note. If an identifier string is used, * it must start with the note letter, optionally followed by an accidental and followed by the * octave number (`"C3"`, `"G#4"`, `"F-1"`, `"Db7"`, etc.). If a number is used, it must be an * integer between 0 and 127. In this case, middle C is considered to be C4 (note number 60). * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should be * explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @throws {Error} Invalid note identifier * @throws {RangeError} Invalid name value * @throws {RangeError} Invalid accidental value * @throws {RangeError} Invalid octave value * @throws {RangeError} Invalid duration value * @throws {RangeError} Invalid attack value * @throws {RangeError} Invalid release value */ constructor(value, options = {}) { // Assign property defaults this.duration = WebMidi.defaults.note.duration; this.attack = WebMidi.defaults.note.attack; this.release = WebMidi.defaults.note.release; // Assign property values from options (validation occurs in setter) if (options.duration != undefined) this.duration = options.duration; if (options.attack != undefined) this.attack = options.attack; if (options.rawAttack != undefined) this.attack = Utilities.from7bitToFloat(options.rawAttack); if (options.release != undefined) this.release = options.release; if (options.rawRelease != undefined) { this.release = Utilities.from7bitToFloat(options.rawRelease); } // Assign note depending on the way it was specified (name or number) if (Number.isInteger(value)) { this.identifier = Utilities.toNoteIdentifier(value); } else { this.identifier = value; } } /** * The name, optional accidental and octave of the note, as a string. * @type {string} * @since 3.0.0 */ get identifier() { return this._name + (this._accidental || "") + this._octave; } set identifier(value) { const fragments = Utilities.getNoteDetails(value); if (WebMidi.validation) { if (!value) throw new Error("Invalid note identifier"); } this._name = fragments.name; this._accidental = fragments.accidental; this._octave = fragments.octave; } /** * The name (letter) of the note. If you need the full name with octave and accidental, you can * use the [`identifier`]{@link Note#identifier} property instead. * @type {string} * @since 3.0.0 */ get name() { return this._name; } set name(value) { if (WebMidi.validation) { value = value.toUpperCase(); if (!["C", "D", "E", "F", "G", "A", "B"].includes(value)) { throw new Error("Invalid name value"); } } this._name = value; } /** * The accidental (#, ##, b or bb) of the note. * @type {string} * @since 3.0.0 */ get accidental() { return this._accidental; } set accidental(value) { if (WebMidi.validation) { value = value.toLowerCase(); if (!["#", "##", "b", "bb"].includes(value)) throw new Error("Invalid accidental value"); } this._accidental = value; } /** * The octave of the note. * @type {number} * @since 3.0.0 */ get octave() { return this._octave; } set octave(value) { if (WebMidi.validation) { value = parseInt(value); if (isNaN(value)) throw new Error("Invalid octave value"); } this._octave = value; } /** * The duration of the note as a positive decimal number representing the number of milliseconds * that the note should play for. * * @type {number} * @since 3.0.0 */ get duration() { return this._duration; } set duration(value) { if (WebMidi.validation) { value = parseFloat(value); if (isNaN(value) || value === null || value < 0) { throw new RangeError("Invalid duration value."); } } this._duration = value; } /** * The attack velocity of the note as a float between 0 and 1. * @type {number} * @since 3.0.0 */ get attack() { return this._attack; } set attack(value) { if (WebMidi.validation) { value = parseFloat(value); if (isNaN(value) || !(value >= 0 && value <= 1)) { throw new RangeError("Invalid attack value."); } } this._attack = value; } /** * The release velocity of the note as an integer between 0 and 1. * @type {number} * @since 3.0.0 */ get release() { return this._release; } set release(value) { if (WebMidi.validation) { value = parseFloat(value); if (isNaN(value) || !(value >= 0 && value <= 1)) { throw new RangeError("Invalid release value."); } } this._release = value; } /** * The attack velocity of the note as a positive integer between 0 and 127. * @type {number} * @since 3.0.0 */ get rawAttack() { return Utilities.fromFloatTo7Bit(this._attack); } set rawAttack(value) { this._attack = Utilities.from7bitToFloat(value); } /** * The release velocity of the note as a positive integer between 0 and 127. * @type {number} * @since 3.0.0 */ get rawRelease() { return Utilities.fromFloatTo7Bit(this._release); } set rawRelease(value) { this._release = Utilities.from7bitToFloat(value); } /** * The MIDI number of the note (`0` - `127`). This number is derived from the note identifier * using C4 as a reference for middle C. * * @type {number} * @readonly * @since 3.0.0 */ get number() { return Utilities.toNoteNumber(this.identifier); } /** * Returns a MIDI note number offset by octave and/or semitone. If the calculated value is less * than 0, 0 will be returned. If the calculated value is more than 127, 127 will be returned. If * an invalid value is supplied, 0 will be used. * * @param [octaveOffset] {number} An integer to offset the note number by octave. * @param [semitoneOffset] {number} An integer to offset the note number by semitone. * @returns {number} An integer between 0 and 127 */ getOffsetNumber(octaveOffset = 0, semitoneOffset = 0) { if (WebMidi.validation) { octaveOffset = parseInt(octaveOffset) || 0; semitoneOffset = parseInt(semitoneOffset) || 0; } return Math.min(Math.max(this.number + (octaveOffset * 12) + semitoneOffset, 0), 127); } } ================================================ FILE: src/Output.js ================================================ import {EventEmitter} from "../node_modules/djipevents/src/djipevents.js"; import {OutputChannel} from "./OutputChannel.js"; import {Enumerations, Message, WebMidi} from "./WebMidi.js"; import {Utilities} from "./Utilities.js"; /** * The `Output` class represents a single MIDI output port (not to be confused with a MIDI channel). * A port is made available by a MIDI device. A MIDI device can advertise several input and output * ports. Each port has 16 MIDI channels which can be accessed via the [`channels`](#channels) * property. * * The `Output` object is automatically instantiated by the library according to the host's MIDI * subsystem and should not be directly instantiated. * * You can access all available `Output` objects by referring to the * [`WebMidi.outputs`](WebMidi#outputs) array or by using methods such as * [`WebMidi.getOutputByName()`](WebMidi#getOutputByName) or * [`WebMidi.getOutputById()`](WebMidi#getOutputById). * * @fires Output#opened * @fires Output#disconnected * @fires Output#closed * * @extends EventEmitter * @license Apache-2.0 */ export class Output extends EventEmitter { /** * Creates an `Output` object. * * @param {MIDIOutput} midiOutput [`MIDIOutput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIOutput) * object as provided by the MIDI subsystem. */ constructor(midiOutput) { super(); /** * A reference to the `MIDIOutput` object * @type {MIDIOutput} * @private */ this._midiOutput = midiOutput; /** * @type {number} * @private */ this._octaveOffset = 0; /** * Array containing the 16 [`OutputChannel`]{@link OutputChannel} objects available provided by * this `Output`. The channels are numbered 1 through 16. * * @type {OutputChannel[]} */ this.channels = []; for (let i = 1; i <= 16; i++) this.channels[i] = new OutputChannel(this, i); this._midiOutput.onstatechange = this._onStateChange.bind(this); } /** * Destroys the `Output`. All listeners are removed, all channels are destroyed and the MIDI * subsystem is unlinked. * @returns {Promise} */ async destroy() { this.removeListener(); this.channels.forEach(ch => ch.destroy()); this.channels = []; if (this._midiOutput) this._midiOutput.onstatechange = null; await this.close(); this._midiOutput = null; } /** * @private */ _onStateChange(e) { let event = { timestamp: WebMidi.time }; if (e.port.connection === "open") { /** * Event emitted when the {@link Output} has been opened by calling the * [open()]{@link Output#open} method. * * @event Output#opened * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `"opened"` * @property {Output} target The object to which the listener was originally added (`Output`). * @property {Output} port The port that was opened */ event.type = "opened"; event.target = this; event.port = event.target; // for consistency this.emit("opened", event); } else if (e.port.connection === "closed" && e.port.state === "connected") { /** * Event emitted when the {@link Output} has been closed by calling the * [close()]{@link Output#close} method. * * @event Output#closed * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `"closed"` * @property {Output} target The object to which the listener was originally added (`Output`). * @property {Output} port The port that was closed */ event.type = "closed"; event.target = this; event.port = event.target; // for consistency this.emit("closed", event); } else if (e.port.connection === "closed" && e.port.state === "disconnected") { /** * Event emitted when the {@link Output} becomes unavailable. This event is typically fired * when the MIDI device is unplugged. * * @event Output#disconnected * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp0 when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type `"disconnected"` * @property {Output} target The object to which the listener was originally added (`Output`). * @property {object} port Object with properties describing the {@link Output} that was * disconnected. This is not the actual `Output` as it is no longer available. */ event.type = "disconnected"; event.port = { connection: e.port.connection, id: e.port.id, manufacturer: e.port.manufacturer, name: e.port.name, state: e.port.state, type: e.port.type }; this.emit("disconnected", event); } else if (e.port.connection === "pending" && e.port.state === "disconnected") { // I don't see the need to forward that... } else { console.warn("This statechange event was not caught:", e.port.connection, e.port.state); } } /** * Opens the output for usage. When the library is enabled, all ports are automatically opened. * This method is only useful for ports that have been manually closed. * * @returns {Promise} The promise is fulfilled with the `Output` object. */ async open() { // Explicitly opens the port for usage. This is not mandatory. When the port is not explicitly // opened, it is implicitly opened (asynchronously) when calling `send()` on the `MIDIOutput`. // We do it explicitly so that 'connected' events are dispatched immediately and we are ready to // send. try { await this._midiOutput.open(); return Promise.resolve(this); } catch (err) { return Promise.reject(err); } } /** * Closes the output connection. When an output is closed, it cannot be used to send MIDI messages * until the output is opened again by calling [`open()`]{@link #open}. You can check * the connection status by looking at the [`connection`]{@link #connection} property. * * @returns {Promise} */ async close() { // We close the port. This triggers a 'statechange' event which we listen to to re-trigger the // 'closed' event. if (this._midiOutput) { await this._midiOutput.close(); } else { await Promise.resolve(); } } /** * Sends a MIDI message on the MIDI output port. If no time is specified, the message will be * sent immediately. The message should be an array of 8 bit unsigned integers (0-225), a * [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array} * object or a [`Message`](Message) object. * * It is usually not necessary to use this method directly as you can use one of the simpler * helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), * [`sendControlChange()`](#sendControlChange), etc. * * Details on the format of MIDI messages are available in the summary of * [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message} * from the MIDI Manufacturers Association. * * @param message {number[]|Uint8Array|Message} An array of 8bit unsigned integers, a `Uint8Array` * object (not available in Node.js) containing the message bytes or a `Message` object. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The first byte (status) must be an integer between 128 and 255. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @license Apache-2.0 */ send(message, options = {time: 0}, legacy = 0) { // If a Message object is passed in we extract the message data (the jzz plugin used on Node.js // does not support using Uint8Array). if (message instanceof Message) { message = Utilities.isNode ? message.data : message.rawData; } // If the data is a Uint8Array and we are on Node, we must convert it to array so it works with // the jzz module. if (message instanceof Uint8Array && Utilities.isNode) { message = Array.from(message); } // Validation if (WebMidi.validation) { // If message is neither an array nor a Uint8Array, then we are in legacy mode if (!Array.isArray(message) && !(message instanceof Uint8Array)) { message = [message]; if (Array.isArray(options)) message = message.concat(options); options = isNaN(legacy) ? {time: 0} : {time: legacy}; } if (!(parseInt(message[0]) >= 128 && parseInt(message[0]) <= 255)) { throw new RangeError("The first byte (status) must be an integer between 128 and 255."); } message.slice(1).forEach(value => { value = parseInt(value); if (!(value >= 0 && value <= 255)) { throw new RangeError("Data bytes must be integers between 0 and 255."); } }); if (!options) options = {time: 0}; } // Send message and return `Output` for chaining this._midiOutput.send(message, Utilities.toTimestamp(options.time)); return this; } /** * Sends a MIDI [**system exclusive**]{@link * https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages} * (*sysex*) message. There are two categories of system exclusive messages: manufacturer-specific * messages and universal messages. Universal messages are further divided into three subtypes: * * * Universal non-commercial (for research and testing): `0x7D` * * Universal non-realtime: `0x7E` * * Universal realtime: `0x7F` * * The method's first parameter (`identification`) identifies the type of message. If the value of * `identification` is `0x7D` (125), `0x7E` (126) or `0x7F` (127), the message will be identified * as a **universal non-commercial**, **universal non-realtime** or **universal realtime** message * (respectively). * * If the `identification` value is an array or an integer between 0 and 124, it will be used to * identify the manufacturer targeted by the message. The *MIDI Manufacturers Association* * maintains a full list of * [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers). * * The `data` parameter should only contain the data of the message. When sending out the actual * MIDI message, WEBMIDI.js will automatically prepend the data with the **sysex byte** (`0xF0`) * and the identification byte(s). It will also automatically terminate the message with the * **sysex end byte** (`0xF7`). * * To use the `sendSysex()` method, system exclusive message support must have been enabled. To * do so, you must set the `sysex` option to `true` when calling * [`WebMidi.enable()`]{@link WebMidi#enable}: * * ```js * WebMidi.enable({sysex: true}) * .then(() => console.log("System exclusive messages are enabled"); * ``` * * ##### Examples of manufacturer-specific system exclusive messages * * If you want to send a sysex message to a Korg device connected to the first output, you would * use the following code: * * ```js * WebMidi.outputs[0].sendSysex(0x42, [0x1, 0x2, 0x3, 0x4, 0x5]); * ``` * In this case `0x42` is the ID of the manufacturer (Korg) and `[0x1, 0x2, 0x3, 0x4, 0x5]` is the * data being sent. * * The parameters can be specified using any number notation (decimal, hex, binary, etc.). * Therefore, the code above is equivalent to this code: * * ```js * WebMidi.outputs[0].sendSysex(66, [1, 2, 3, 4, 5]); * ``` * * Some manufacturers are identified using 3 bytes. In this case, you would use a 3-position array * as the first parameter. For example, to send the same sysex message to a * *Native Instruments* device: * * ```js * WebMidi.outputs[0].sendSysex([0x00, 0x21, 0x09], [0x1, 0x2, 0x3, 0x4, 0x5]); * ``` * * There is no limit for the length of the data array. However, it is generally suggested to keep * system exclusive messages to 64Kb or less. * * ##### Example of universal system exclusive message * * If you want to send a universal sysex message, simply assign the correct identification number * in the first parameter. Number `0x7D` (125) is for non-commercial, `0x7E` (126) is for * non-realtime and `0x7F` (127) is for realtime. * * So, for example, if you wanted to send an identity request non-realtime message (`0x7E`), you * could use the following: * * ```js * WebMidi.outputs[0].sendSysex(0x7E, [0x7F, 0x06, 0x01]); * ``` * * For more details on the format of universal messages, consult the list of * [universal sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages). * * @param {number|number[]} identification An unsigned integer or an array of three unsigned * integers between `0` and `127` that either identify the manufacturer or sets the message to be * a **universal non-commercial message** (`0x7D`), a **universal non-realtime message** (`0x7E`) * or a **universal realtime message** (`0x7F`). The *MIDI Manufacturers Association* maintains a * full list of * [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers). * * @param {number[]|Uint8Array} [data] A `Uint8Array` or an array of unsigned integers between `0` * and `127`. This is the data you wish to transfer. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {DOMException} Failed to execute 'send' on 'MIDIOutput': System exclusive message is * not allowed. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index x is greater * than 0xFF. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendSysex(identification, data= [], options = {}) { identification = [].concat(identification); // Check if data is Uint8Array if (data instanceof Uint8Array) { const merged = new Uint8Array(1 + identification.length + data.length + 1); merged[0] = Enumerations.SYSTEM_MESSAGES.sysex; merged.set(Uint8Array.from(identification), 1); merged.set(data, 1 + identification.length); merged[merged.length - 1] = Enumerations.SYSTEM_MESSAGES.sysexend; this.send(merged, {time: options.time}); } else { const merged = identification.concat(data, Enumerations.SYSTEM_MESSAGES.sysexend); this.send([Enumerations.SYSTEM_MESSAGES.sysex].concat(merged), {time: options.time}); } return this; }; /** * Clears all MIDI messages that have been queued and scheduled but not yet sent. * * **Warning**: this method is defined in the * [Web MIDI API specification](https://www.w3.org/TR/webmidi/#MIDIOutput) but has not been * implemented by all browsers yet. You can follow * [this issue](https://github.com/djipco/webmidi/issues/52) for more info. * * @returns {Output} Returns the `Output` object so methods can be chained. */ clear() { if (this._midiOutput.clear) { this._midiOutput.clear(); } else { if (WebMidi.validation) { console.warn( "The 'clear()' method has not yet been implemented in your environment." ); } } return this; } /** * Sends a MIDI **timecode quarter frame** message. Please note that no processing is being done * on the data. It is up to the developer to format the data according to the * [MIDI Timecode](https://en.wikipedia.org/wiki/MIDI_timecode) format. * * @param value {number} The quarter frame message content (integer between 0 and 127). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendTimecodeQuarterFrame(value, options = {}) { if (WebMidi.validation) { value = parseInt(value); if (isNaN(value) || !(value >= 0 && value <= 127)) { throw new RangeError("The value must be an integer between 0 and 127."); } } this.send( [ Enumerations.SYSTEM_MESSAGES.timecode, value ], {time: options.time} ); return this; }; /** * Sends a **song position** MIDI message. The value is expressed in MIDI beats (between `0` and * `16383`) which are 16th note. Position `0` is always the start of the song. * * @param {number} [value=0] The MIDI beat to cue to (integer between `0` and `16383`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendSongPosition(value = 0, options = {}) { // @todo allow passing in 2-entries array for msb/lsb value = Math.floor(value) || 0; var msb = (value >> 7) & 0x7F; var lsb = value & 0x7F; this.send( [ Enumerations.SYSTEM_MESSAGES.songposition, msb, lsb ], {time: options.time} ); return this; } /** * Sends a **song select** MIDI message. * * @param {number} [value=0] The number of the song to select (integer between `0` and `127`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws The song number must be between 0 and 127. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendSongSelect(value = 0, options = {}) { if (WebMidi.validation) { value = parseInt(value); if (isNaN(value) || !(value >= 0 && value <= 127)) { throw new RangeError("The program value must be between 0 and 127"); } } this.send( [ Enumerations.SYSTEM_MESSAGES.songselect, value ], {time: options.time} ); return this; } /** * Sends a MIDI **tune request** real-time message. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuneRequest(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.tunerequest], {time: options.time} ); return this; } /** * Sends a MIDI **clock** real-time message. According to the standard, there are 24 MIDI clocks * for every quarter note. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendClock(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.clock], {time: options.time} ); return this; } /** * Sends a **start** real-time message. A MIDI Start message starts the playback of the current * song at beat 0. To start playback elsewhere in the song, use the * [`sendContinue()`]{@link #sendContinue} method. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendStart(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.start], {time: options.time} ); return this; } /** * Sends a **continue** real-time message. This resumes song playback where it was previously * stopped or where it was last cued with a song position message. To start playback from the * start, use the [`sendStart()`]{@link Output#sendStart}` method. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendContinue(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.continue], {time: options.time} ); return this; } /** * Sends a **stop** real-time message. This tells the device connected to this output to stop * playback immediately (or at the scheduled time, if specified). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendStop(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.stop], {time: options.time} ); return this; } /** * Sends an **active sensing** real-time message. This tells the device connected to this port * that the connection is still good. Active sensing messages are often sent every 300 ms if there * was no other activity on the MIDI port. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendActiveSensing(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.activesensing], {time: options.time} ); return this; } /** * Sends a **reset** real-time message. This tells the device connected to this output that it * should reset itself to a default state. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendReset(options = {}) { this.send( [Enumerations.SYSTEM_MESSAGES.reset], {time: options.time} ); return this; } /** * @private * @deprecated since version 3.0 */ sendTuningRequest(options = {}) { if (WebMidi.validation) { console.warn( "The sendTuningRequest() method has been deprecated. Use sendTuningRequest() instead." ); } return this.sendTuneRequest(options); } /** * Sends a MIDI **key aftertouch** message to the specified channel(s) at the scheduled time. This * is a key-specific aftertouch. For a channel-wide aftertouch message, use * [`setChannelAftertouch()`]{@link #setChannelAftertouch}. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending * an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a * [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the * previous types. When using a note identifier, octave range must be between `-1` and `9`. The * lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number * `127`). * * @param [pressure=0.5] {number} The pressure level (between 0 and 1). An invalid pressure value * will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the * pressure can be defined by using an integer between 0 and 127. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendKeyAftertouch(note, pressure, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendKeyAftertouch(note, pressure, options); }); return this; }; /** * Sends a MIDI **control change** message to the specified channel(s) at the scheduled time. The * control change message to send can be specified numerically (0-127) or by using one of the * following common names: * * | Number | Name | * |--------|-------------------------------| * | 0 |`bankselectcoarse` | * | 1 |`modulationwheelcoarse` | * | 2 |`breathcontrollercoarse` | * | 4 |`footcontrollercoarse` | * | 5 |`portamentotimecoarse` | * | 6 |`dataentrycoarse` | * | 7 |`volumecoarse` | * | 8 |`balancecoarse` | * | 10 |`pancoarse` | * | 11 |`expressioncoarse` | * | 12 |`effectcontrol1coarse` | * | 13 |`effectcontrol2coarse` | * | 18 |`generalpurposeslider3` | * | 19 |`generalpurposeslider4` | * | 32 |`bankselectfine` | * | 33 |`modulationwheelfine` | * | 34 |`breathcontrollerfine` | * | 36 |`footcontrollerfine` | * | 37 |`portamentotimefine` | * | 38 |`dataentryfine` | * | 39 |`volumefine` | * | 40 |`balancefine` | * | 42 |`panfine` | * | 43 |`expressionfine` | * | 44 |`effectcontrol1fine` | * | 45 |`effectcontrol2fine` | * | 64 |`holdpedal` | * | 65 |`portamento` | * | 66 |`sustenutopedal` | * | 67 |`softpedal` | * | 68 |`legatopedal` | * | 69 |`hold2pedal` | * | 70 |`soundvariation` | * | 71 |`resonance` | * | 72 |`soundreleasetime` | * | 73 |`soundattacktime` | * | 74 |`brightness` | * | 75 |`soundcontrol6` | * | 76 |`soundcontrol7` | * | 77 |`soundcontrol8` | * | 78 |`soundcontrol9` | * | 79 |`soundcontrol10` | * | 80 |`generalpurposebutton1` | * | 81 |`generalpurposebutton2` | * | 82 |`generalpurposebutton3` | * | 83 |`generalpurposebutton4` | * | 91 |`reverblevel` | * | 92 |`tremololevel` | * | 93 |`choruslevel` | * | 94 |`celestelevel` | * | 95 |`phaserlevel` | * | 96 |`dataincrement` | * | 97 |`datadecrement` | * | 98 |`nonregisteredparametercoarse` | * | 99 |`nonregisteredparameterfine` | * | 100 |`registeredparametercoarse` | * | 101 |`registeredparameterfine` | * | 120 |`allsoundoff` | * | 121 |`resetallcontrollers` | * | 122 |`localcontrol` | * | 123 |`allnotesoff` | * | 124 |`omnimodeoff` | * | 125 |`omnimodeon` | * | 126 |`monomodeon` | * | 127 |`polymodeon` | * * Note: as you can see above, not all control change message have a matching name. This does not * mean you cannot use the others. It simply means you will need to use their number (`0` - `127`) * instead of their name. While you can still use them, numbers `120` to `127` are usually * reserved for *channel mode* messages. See [`sendChannelMode()`]{@link #sendChannelMode} method * for more info. * * To view a list of all available **control change** messages, please consult [Table 3 - Control * Change Messages](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * from the MIDI specification. * * @param controller {number|string} The MIDI controller name or number (0-127). * * @param [value=0] {number} The value to send (0-127). * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} Controller numbers must be between 0 and 127. * @throws {RangeError} Invalid controller name. * * @return {Output} Returns the `Output` object so methods can be chained. */ sendControlChange(controller, value, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendControlChange(controller, value, options); }); return this; }; /** * Sends a **pitch bend range** message to the specified channel(s) at the scheduled time so that * they adjust the range used by their pitch bend lever. The range is specified by using the * `semitones` and `cents` parameters. For example, setting the `semitones` parameter to `12` * means that the pitch bend range will be 12 semitones above and below the nominal pitch. * * @param {number} [semitones=0] The desired adjustment value in semitones (between `0` and `127`). * While nothing imposes that in the specification, it is very common for manufacturers to limit * the range to 2 octaves (-12 semitones to 12 semitones). * * @param {number} [cents=0] The desired adjustment value in cents (integer between `0` and * `127`). * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The msb value must be between 0 and 127. * @throws {RangeError} The lsb value must be between 0 and 127. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPitchBendRange(semitones= 0, cents = 0, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendPitchBendRange(semitones, cents, options); }); return this; } /** * @private * @deprecated since version 3.0 */ setPitchBendRange(semitones = 0, cents = 0, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setPitchBendRange() method is deprecated. Use sendPitchBendRange() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendPitchBendRange(semitones, cents, options); } /** * Sets the specified MIDI registered parameter to the desired value. The value is defined with * up to two bytes of data (msb, lsb) that each can go from `0` to `127`. * * MIDI * [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * extend the original list of control change messages. The MIDI 1.0 specification lists only a * limited number of them: * * | Numbers | Function | * |--------------|--------------------------| * | (0x00, 0x00) | `pitchbendrange` | * | (0x00, 0x01) | `channelfinetuning` | * | (0x00, 0x02) | `channelcoarsetuning` | * | (0x00, 0x03) | `tuningprogram` | * | (0x00, 0x04) | `tuningbank` | * | (0x00, 0x05) | `modulationrange` | * | (0x3D, 0x00) | `azimuthangle` | * | (0x3D, 0x01) | `elevationangle` | * | (0x3D, 0x02) | `gain` | * | (0x3D, 0x03) | `distanceratio` | * | (0x3D, 0x04) | `maximumdistance` | * | (0x3D, 0x05) | `maximumdistancegain` | * | (0x3D, 0x06) | `referencedistanceratio` | * | (0x3D, 0x07) | `panspreadangle` | * | (0x3D, 0x08) | `rollangle` | * * Note that the `tuningprogram` and `tuningbank` parameters are part of the *MIDI Tuning * Standard*, which is not widely implemented. * * @param parameter {string|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the * registered parameter. * * @param [data=[]] {number|number[]} A single integer or an array of integers with a maximum * length of 2 specifying the desired data. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnValue(parameter, data, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendRpnValue(parameter, data, options); }); return this; } /** * @private * @deprecated since version 3.0 */ setRegisteredParameter(parameter, data = [], channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setRegisteredParameter() method is deprecated. Use sendRpnValue() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendRpnValue(parameter, data, options); } /** * Sends a MIDI **channel aftertouch** message to the specified channel(s). For key-specific * aftertouch, you should instead use [`setKeyAftertouch()`]{@link #setKeyAftertouch}. * * @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure * value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, * the pressure can be defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * @since 3.0.0 */ sendChannelAftertouch(pressure, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendChannelAftertouch(pressure, options); }); return this; } /** * Sends a MIDI **pitch bend** message to the specified channel(s) at the scheduled time. * * The resulting bend is relative to the pitch bend range that has been defined. The range can be * set with [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch * bend range has been set to 12 semitones, using a bend value of `-1` will bend the note 1 octave * below its nominal value. * * @param {number|number[]} value The intensity of the bend (between `-1.0` and `1.0`). A value of * `0` means no bend. If an invalid value is specified, the nearest valid value will be used * instead. If the `rawValue` option is set to `true`, the intensity of the bend can be defined by * either using a single integer between `0` and `127` (MSB) or an array of two integers between * `0` and `127` representing, respectively, the MSB (most significant byte) and the LSB (least * significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value lower * than `64` bends downwards while a value higher than `64` bends upwards. The LSB is expressed * in cents (1/100 of a semitone). An LSB of `64` also means no bend. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered as a float between `-1.0` and `1.0` (default) or as raw integer between `0` and * 127` (or an array of 2 integers if using both MSB and LSB). * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPitchBend(value, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendPitchBend(value, options); }); return this; } /** * Sends a MIDI **program change** message to the specified channel(s) at the scheduled time. * * @param {number} [program=0] The MIDI patch (program) number (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater * than 0xFF. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendProgramChange(program = 0, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendProgramChange(program, options); }); return this; } /** * Sends a **modulation depth range** message to the specified channel(s) so that they adjust the * depth of their modulation wheel's range. The range can be specified with the `semitones` * parameter, the `cents` parameter or by specifying both parameters at the same time. * * @param [semitones=0] {number} The desired adjustment value in semitones (integer between * 0 and 127). * * @param [cents=0] {number} The desired adjustment value in cents (integer between 0 and 127). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The msb value must be between 0 and 127 * @throws {RangeError} The lsb value must be between 0 and 127 * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendModulationRange(semitones, cents, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendModulationRange(semitones, cents, options); }); return this; }; /** * @private * @deprecated since version 3.0 */ setModulationRange(semitones = 0, cents = 0, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setModulationRange() method is deprecated. Use sendModulationRange() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendModulationRange(semitones, cents, options); } /** * Sends a master tuning message to the specified channel(s). The value is decimal and must be * larger than `-65` semitones and smaller than `64` semitones. * * Because of the way the MIDI specification works, the decimal portion of the value will be * encoded with a resolution of 14bit. The integer portion must be between -64 and 63 * inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and * a **Master Fine Tuning** RPN messages. * * @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64) * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The value must be a decimal number between larger than -65 and smaller * than 64. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendMasterTuning(value, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendMasterTuning(value, options); }); return this; } /** * @private * @deprecated since version 3.0 */ setMasterTuning(value, channel = {}, options = {}) { if (WebMidi.validation) { console.warn( "The setMasterTuning() method is deprecated. Use sendMasterTuning() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendMasterTuning(value, options); } /** * Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning program (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The program value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuningProgram(value, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendTuningProgram(value, options); }); return this; } /** * @private * @deprecated since version 3.0 */ setTuningProgram(value, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setTuningProgram() method is deprecated. Use sendTuningProgram() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendTuningProgram(value, options); } /** * Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param {number} [value=0] The desired tuning bank (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The bank value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuningBank(value= 0, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendTuningBank(value, options); }); return this; }; /** * @private * @deprecated since version 3.0 */ setTuningBank(parameter, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setTuningBank() method is deprecated. Use sendTuningBank() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendTuningBank(parameter, options); } /** * Sends a MIDI **channel mode** message to the specified channel(s). The channel mode message to * send can be specified numerically or by using one of the following common names: * * | Type |Number| Shortcut Method | * | ---------------------|------|-------------------------------------------------------------- | * | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} | * | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} | * | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} | * | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} | * | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} | * | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} | * | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} | * | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} | * * Note: as you can see above, to make it easier, all channel mode messages also have a matching * helper method. * * It should also be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` * may require a value that's not zero. For that reason, the `value` parameter is optional and * defaults to 0. * * @param {number|string} command The numerical identifier of the channel mode message (integer * between 120-127) or its name as a string. * * @param {number} [value=0] The value to send (integer between 0-127). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * */ sendChannelMode(command, value = 0, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendChannelMode(command, value, options); }); return this; } /** * Sends an **all sound off** channel mode message. This will silence all sounds playing on that * channel but will not prevent new sounds from being triggered. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} * * @since 3.0.0 */ sendAllSoundOff(options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendAllSoundOff(options); }); return this; } /** * Sends an **all notes off** channel mode message. This will make all currently playing notes * fade out just as if their key had been released. This is different from the * [`sendAllSoundOff()`]{@link #sendAllSoundOff} method which mutes all sounds immediately. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} * * @since 3.0.0 */ sendAllNotesOff(options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendAllNotesOff(options); }); return this; } /** * Sends a **reset all controllers** channel mode message. This resets all controllers, such as * the pitch bend, to their default value. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} */ sendResetAllControllers(options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendResetAllControllers(options); }); return this; } /** * Sets the polyphonic mode. In `poly` mode (usually the default), multiple notes can be played * and heard at the same time. In `mono` mode, only one note will be heard at once even if * multiple notes are being played. * * @param mode {string} The mode to use: `mono` or `poly`. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPolyphonicMode(mode, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendPolyphonicMode(mode, options); }); return this; } /** * Turns local control on or off. Local control is usually enabled by default. If you disable it, * the instrument will no longer trigger its own sounds. It will only send the MIDI messages to * its out port. * * @param [state=false] {boolean} Whether to activate local control (`true`) or disable it * (`false`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendLocalControl(state, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendLocalControl(state, options); }); return this; } /** * Sets OMNI mode to **on** or **off** for the specified channel(s). MIDI's OMNI mode causes the * instrument to respond to messages from all channels. * * It should be noted that support for OMNI mode is not as common as it used to be. * * @param [state] {boolean} Whether to activate OMNI mode (`true`) or not (`false`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendOmniMode(state, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendOmniMode(state, options); }); return this; } /** * Sets a non-registered parameter to the specified value. The NRPN is selected by passing a * two-position array specifying the values of the two control bytes. The value is specified by * passing a single integer (most cases) or an array of two integers. * * NRPNs are not standardized in any way. Each manufacturer is free to implement them any way * they see fit. For example, according to the Roland GS specification, you can control the * **vibrato rate** using NRPN (`1`, `8`). Therefore, to set the **vibrato rate** value to `123` * you would use: * * ```js * WebMidi.outputs[0].sendNrpnValue([1, 8], 123); * ``` * * You probably want to should select a channel so the message is not sent to all channels. For * instance, to send to channel `1` of the first output port, you would use: * * ```js * WebMidi.outputs[0].sendNrpnValue([1, 8], 123, 1); * ``` * * In some rarer cases, you need to send two values with your NRPN messages. In such cases, you * would use a 2-position array. For example, for its **ClockBPM** parameter (`2`, `63`), Novation * uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the * value to send was `10`, you could use: * * ```js * WebMidi.outputs[0].sendNrpnValue([2, 63], [0, 10], 1); * ``` * * For further implementation details, refer to the manufacturer's documentation. * * @param parameter {number[]} A two-position array specifying the two control bytes (`0x63`, * `0x62`) that identify the non-registered parameter. * * @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2 * specifying the desired data. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The control value must be between 0 and 127. * @throws {RangeError} The msb value must be between 0 and 127 * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNrpnValue(parameter, data, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendNrpnValue(parameter, data, options); }); return this; } /** * @private * @deprecated since version 3.0 */ setNonRegisteredParameter(parameter, data = [], channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The setNonRegisteredParameter() method is deprecated. Use sendNrpnValue() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendNrpnValue(parameter, data, options); } /** * Increments the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this method: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnIncrement(parameter, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendRpnIncrement(parameter, options); }); return this; } /** * @private * @deprecated since version 3.0 */ incrementRegisteredParameter(parameter, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The incrementRegisteredParameter() method is deprecated. Use sendRpnIncrement() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendRpnIncrement(parameter, options); } /** * Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this method: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified parameter is not available. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnDecrement(parameter, options = {}) { if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendRpnDecrement(parameter, options); }); return this; } /** * @private * @deprecated since version 3.0 */ decrementRegisteredParameter(parameter, channel = "all", options = {}) { if (WebMidi.validation) { console.warn( "The decrementRegisteredParameter() method is deprecated. Use sendRpnDecrement() instead." ); options.channels = channel; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return this.sendRpnDecrement(parameter, options); } /** * Sends a **note off** message for the specified MIDI note number on the specified channel(s). * The first parameter is the note to stop. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, * `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range * must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest * note is `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNoteOff(note, options= {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendNoteOff(note, options); }); return this; } /** * Sends a **note off** message for the specified MIDI note number on the specified channel(s). * The first parameter is the note to stop. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ stopNote(note, options) { return this.sendNoteOff(note, options); } /** * Plays a note or an array of notes on one or more channels of this output. If you intend to play * notes on a single channel, you should probably use * [`OutputChannel.playNote()`](OutputChannel#playNote) instead. * * The first parameter is the note to play. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`]{@link Note} object * * The `playNote()` method sends a **note on** MIDI message for all specified notes on all * specified channels. If no channel is specified, it will send to all channels. If a `duration` * is set in the `options` parameter or in the [`Note`]{@link Note} object's * [`duration`]{@link Note#duration} property, it will also schedule a **note off** message to end * the note after said duration. If no `duration` is set, the note will simply play until a * matching **note off** message is sent with [`stopNote()`]{@link #stopNote}. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the durations and velocities defined in the * [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options` * parameter. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. When using a note identifier, * octave range must be between -1 and 9. The lowest note is C-1 (MIDI note number `0`) and the * highest note is G9 (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.duration=undefined] The number of milliseconds after which a * **note off** message will be scheduled. If left undefined, only a **note on** message is sent. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity * value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between * `0` and `127`). This has priority over the `attack` property. An invalid velocity value will * silently trigger the default of 64. * * @param {number} [options.release=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid * velocity value will silently trigger the default of `0.5`. This is only used with the * **note off** event triggered when `options.duration` is set. * * @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0` * and `127`). This has priority over the `release` property. An invalid velocity value will * silently trigger the default of 64. This is only used with the **note off** event triggered * when `options.duration` is set. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ playNote(note, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy-compatibility warnings if (options.rawVelocity) { console.warn("The 'rawVelocity' option is deprecated. Use 'rawAttack' instead."); } if (options.velocity) { console.warn("The 'velocity' option is deprecated. Use 'velocity' instead."); } // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].playNote(note, options); }); return this; } /** * Sends a **note on** message for the specified MIDI note number on the specified channel(s). The * first parameter is the number. It can be a single value or an array of the following valid * values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0` * and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNoteOn(note, options = {}, legacy = {}) { if (WebMidi.validation) { // Legacy compatibility if (Array.isArray(options) || Number.isInteger(options) || options === "all") { const channels = options; options = legacy; options.channels = channels; if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; } } if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS; // This actually supports passing a Note object even if, semantically, this does not make sense. Utilities.sanitizeChannels(options.channels).forEach(ch => { this.channels[ch].sendNoteOn(note, options); }); return this; } /** * Name of the MIDI output. * * @type {string} * @readonly */ get name() { return this._midiOutput.name; } /** * ID string of the MIDI output. The ID is host-specific. Do not expect the same ID on different * platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for * the same port. * * @type {string} * @readonly */ get id() { return this._midiOutput.id; } /** * Output port's connection state: `pending`, `open` or `closed`. * * @type {string} * @readonly */ get connection() { return this._midiOutput.connection; } /** * Name of the manufacturer of the device that makes this output port available. * * @type {string} * @readonly */ get manufacturer() { return this._midiOutput.manufacturer; } /** * State of the output port: `connected` or `disconnected`. * * @type {string} * @readonly */ get state() { return this._midiOutput.state; } /** * Type of the output port (it will always be: `output`). * * @type {string} * @readonly */ get type() { return this._midiOutput.type; } /** * An integer to offset the octave of outgoing notes. By default, middle C (MIDI note number 60) * is placed on the 4th octave (C4). * * Note that this value is combined with the global offset value defined in * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) (if any). * * @type {number} * * @since 3.0 */ get octaveOffset() { return this._octaveOffset; } set octaveOffset(value) { if (this.validation) { value = parseInt(value); if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer."); } this._octaveOffset = value; } } ================================================ FILE: src/OutputChannel.js ================================================ import {EventEmitter} from "../node_modules/djipevents/src/djipevents.js"; import {WebMidi} from "./WebMidi.js"; import {Utilities} from "./Utilities.js"; import {Enumerations} from "./Enumerations.js"; /** * The `OutputChannel` class represents a single output MIDI channel. `OutputChannel` objects are * provided by an [`Output`](Output) port which, itself, is made available by a device. The * `OutputChannel` object is derived from the host's MIDI subsystem and should not be instantiated * directly. * * All 16 `OutputChannel` objects can be found inside the parent output's * [`channels`]{@link Output#channels} property. * * @param {Output} output The [`Output`](Output) this channel belongs to. * @param {number} number The MIDI channel number (`1` - `16`). * * @extends EventEmitter * @license Apache-2.0 * @since 3.0.0 */ export class OutputChannel extends EventEmitter { /** * Creates an `OutputChannel` object. * * @param {Output} output The [`Output`](Output) this channel belongs to. * @param {number} number The MIDI channel number (`1` - `16`). */ constructor(output, number) { super(); /** * @type {Output} * @private */ this._output = output; /** * @type {number} * @private */ this._number = number; /** * @type {number} * @private */ this._octaveOffset = 0; } /** * Unlinks the MIDI subsystem, removes all listeners attached to the channel and nulls the channel * number. This method is mostly for internal use. It has not been prefixed with an underscore * since it is called by other objects such as the `Output` object. * * @private */ destroy() { this._output = null; this._number = null; this._octaveOffset = 0; this.removeListener(); } /** * Sends a MIDI message on the MIDI output port. If no time is specified, the message will be * sent immediately. The message should be an array of 8-bit unsigned integers (`0` - `225`), * a * [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array} * object or a [`Message`](Message) object. * * It is usually not necessary to use this method directly as you can use one of the simpler * helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), * [`sendControlChange()`](#sendControlChange), etc. * * Details on the format of MIDI messages are available in the summary of * [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message} * from the MIDI Manufacturers Association. * * @param message {number[]|Uint8Array|Message} A `Message` object, an array of 8-bit unsigned * integers or a `Uint8Array` object (not available in Node.js) containing the message bytes. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The first byte (status) must be an integer between 128 and 255. * * @throws {RangeError} Data bytes must be integers between 0 and 255. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ send(message, options = {time: 0}) { this.output.send(message, options); return this; } /** * Sends a MIDI **key aftertouch** message at the scheduled time. This is a key-specific * aftertouch. For a channel-wide aftertouch message, use * [`sendChannelAftertouch()`]{@link #sendChannelAftertouch}. * * @param target {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending * an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a * [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the * previous types. When using a note identifier, octave range must be between `-1` and `9`. The * lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number * `127`). * * When using a note identifier, the octave value will be offset by the local * [`octaveOffset`](#octaveOffset) and by * [`Output.octaveOffset`](Output#octaveOffset) and [`WebMidi.octaveOffset`](WebMidi#octaveOffset) * (if those values are not `0`). When using a key number, `octaveOffset` values are ignored. * * @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure * value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, * the pressure is defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @throws RangeError Invalid key aftertouch value. */ sendKeyAftertouch(target, pressure, options = {}) { if (WebMidi.validation) { // Legacy support if (options.useRawValue) options.rawValue = options.useRawValue; if (isNaN(parseFloat(pressure))) { throw new RangeError("Invalid key aftertouch value."); } if (options.rawValue) { if (!(pressure >= 0 && pressure <= 127 && Number.isInteger(pressure))) { throw new RangeError("Key aftertouch raw value must be an integer between 0 and 127."); } } else { if (!(pressure >= 0 && pressure <= 1)) { throw new RangeError("Key aftertouch value must be a float between 0 and 1."); } } } // Normalize pressure to integer if (!options.rawValue) pressure = Utilities.fromFloatTo7Bit(pressure); // Plot total offset const offset = WebMidi.octaveOffset + this.output.octaveOffset + this.octaveOffset; // Make sure we are dealing with an array if (!Array.isArray(target)) target = [target]; Utilities.buildNoteArray(target).forEach(n => { this.send( [ (Enumerations.CHANNEL_MESSAGES.keyaftertouch << 4) + (this.number - 1), n.getOffsetNumber(offset), pressure ], {time: Utilities.toTimestamp(options.time)} ); }); return this; } /** * Sends a MIDI **control change** message to the channel at the scheduled time. The control * change message to send can be specified numerically (`0` to `127`) or by using one of the * following common names: * * | Number | Name | * |--------|-------------------------------| * | 0 |`bankselectcoarse` | * | 1 |`modulationwheelcoarse` | * | 2 |`breathcontrollercoarse` | * | 4 |`footcontrollercoarse` | * | 5 |`portamentotimecoarse` | * | 6 |`dataentrycoarse` | * | 7 |`volumecoarse` | * | 8 |`balancecoarse` | * | 10 |`pancoarse` | * | 11 |`expressioncoarse` | * | 12 |`effectcontrol1coarse` | * | 13 |`effectcontrol2coarse` | * | 18 |`generalpurposeslider3` | * | 19 |`generalpurposeslider4` | * | 32 |`bankselectfine` | * | 33 |`modulationwheelfine` | * | 34 |`breathcontrollerfine` | * | 36 |`footcontrollerfine` | * | 37 |`portamentotimefine` | * | 38 |`dataentryfine` | * | 39 |`volumefine` | * | 40 |`balancefine` | * | 42 |`panfine` | * | 43 |`expressionfine` | * | 44 |`effectcontrol1fine` | * | 45 |`effectcontrol2fine` | * | 64 |`holdpedal` | * | 65 |`portamento` | * | 66 |`sustenutopedal` | * | 67 |`softpedal` | * | 68 |`legatopedal` | * | 69 |`hold2pedal` | * | 70 |`soundvariation` | * | 71 |`resonance` | * | 72 |`soundreleasetime` | * | 73 |`soundattacktime` | * | 74 |`brightness` | * | 75 |`soundcontrol6` | * | 76 |`soundcontrol7` | * | 77 |`soundcontrol8` | * | 78 |`soundcontrol9` | * | 79 |`soundcontrol10` | * | 80 |`generalpurposebutton1` | * | 81 |`generalpurposebutton2` | * | 82 |`generalpurposebutton3` | * | 83 |`generalpurposebutton4` | * | 91 |`reverblevel` | * | 92 |`tremololevel` | * | 93 |`choruslevel` | * | 94 |`celestelevel` | * | 95 |`phaserlevel` | * | 96 |`dataincrement` | * | 97 |`datadecrement` | * | 98 |`nonregisteredparametercoarse` | * | 99 |`nonregisteredparameterfine` | * | 100 |`registeredparametercoarse` | * | 101 |`registeredparameterfine` | * | 120 |`allsoundoff` | * | 121 |`resetallcontrollers` | * | 122 |`localcontrol` | * | 123 |`allnotesoff` | * | 124 |`omnimodeoff` | * | 125 |`omnimodeon` | * | 126 |`monomodeon` | * | 127 |`polymodeon` | * * As you can see above, not all control change message have a matching name. This does not mean * you cannot use the others. It simply means you will need to use their number * (`0` to `127`) instead of their name. While you can still use them, numbers `120` to `127` are * usually reserved for *channel mode* messages. See * [`sendChannelMode()`]{@link OutputChannel#sendChannelMode} method for more info. * * To view a detailed list of all available **control change** messages, please consult "Table 3 - * Control Change Messages" from the [MIDI Messages]( * https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2) * specification. * * **Note**: messages #0-31 (MSB) are paired with messages #32-63 (LSB). For example, message #1 * (`modulationwheelcoarse`) can be accompanied by a second control change message for * `modulationwheelfine` to achieve a greater level of precision. if you want to specify both MSB * and LSB for messages between `0` and `31`, you can do so by passing a 2-value array as the * second parameter. * * @param {number|string} controller The MIDI controller name or number (`0` - `127`). * * @param {number|number[]} value The value to send (0-127). You can also use a two-position array * for controllers 0 to 31. In this scenario, the first value will be sent as usual and the second * value will be sent to the matching LSB controller (which is obtained by adding 32 to the first * controller) * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} Controller numbers must be between 0 and 127. * @throws {RangeError} Invalid controller name. * @throws {TypeError} The value array must have a length of 2. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @license Apache-2.0 * @since 3.0.0 */ sendControlChange(controller, value, options = {}) { if (typeof controller === "string") { controller = Utilities.getCcNumberByName(controller); } if (!Array.isArray(value)) value = [value]; if (WebMidi.validation) { if (controller === undefined) { throw new TypeError( "Control change must be identified with a valid name or an integer between 0 and 127." ); } if (!Number.isInteger(controller) || !(controller >= 0 && controller <= 127)) { throw new TypeError("Control change number must be an integer between 0 and 127."); } value = value.map(item => { const output = Math.min(Math.max(parseInt(item), 0), 127); if (isNaN(output)) throw new TypeError("Values must be integers between 0 and 127"); return output; }); if (value.length === 2 && controller >= 32) { throw new TypeError("To use a value array, the controller must be between 0 and 31"); } } value.forEach((item, index) => { this.send( [ (Enumerations.CHANNEL_MESSAGES.controlchange << 4) + (this.number - 1), controller + (index * 32), value[index] ], {time: Utilities.toTimestamp(options.time)} ); }); return this; } /** * Selects a MIDI non-registered parameter so it is affected by upcoming data entry, data * increment and data decrement messages. * * @param parameter {number[]} A two-position array specifying the two control bytes that identify * the registered parameter. The NRPN MSB (99 or 0x63) is a position 0. The NRPN LSB (98 or 0x62) * is at position 1. * * @private * * @param {object} [options={}] * * @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by * a number, the message will be delayed by that many milliseconds. If the value is a number, the * operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ _selectNonRegisteredParameter(parameter, options = {}) { // parameter[0] = Math.floor(parameter[0]); // if (!(parameter[0] >= 0 && parameter[0] <= 127)) { // throw new RangeError("The control63 value must be between 0 and 127."); // } // // parameter[1] = Math.floor(parameter[1]); // if (!(parameter[1] >= 0 && parameter[1] <= 127)) { // throw new RangeError("The control62 value must be between 0 and 127."); // } this.sendControlChange(0x63, parameter[0], options); this.sendControlChange(0x62, parameter[1], options); return this; } /** * Deselects the currently active MIDI registered parameter so it is no longer affected by data * entry, data increment and data decrement messages. * * Current best practice recommends doing that after each call to * [_setCurrentParameter()]{@link #_setCurrentParameter}. * * @private * * @param {object} [options={}] * * @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by * a number, the message will be delayed by that many milliseconds. If the value is a number, the * operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ _deselectRegisteredParameter(options = {}) { this.sendControlChange(0x65, 0x7F, options); this.sendControlChange(0x64, 0x7F, options); return this; } /** * Deselects the currently active MIDI non-registered parameter so it is no longer affected by * data entry, data increment and data decrement messages. * * @private * * @param {object} [options={}] * * @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by * a number, the message will be delayed by that many milliseconds. If the value is a number, the * operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ _deselectNonRegisteredParameter(options = {}) { this.sendControlChange(0x65, 0x7F, options); this.sendControlChange(0x64, 0x7F, options); return this; } /** * Selects a MIDI registered parameter so it is affected by upcoming data entry, data increment * and data decrement messages. * * @private * * @param parameter {number[]} A two-position array of integers specifying the two control bytes * (0x65, 0x64) that identify the registered parameter. The integers must be between 0 and 127. * * @param {object} [options={}] * * @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by * a number, the message will be delayed by that many milliseconds. If the value is a number, the * operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ _selectRegisteredParameter(parameter, options = {}) { this.sendControlChange(0x65, parameter[0], options); this.sendControlChange(0x64, parameter[1], options); return this; } /** * Sets the value of the currently selected MIDI registered parameter. * * @private * * @param data {number|number[]} * * @param {object} [options={}] * * @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by * a number, the message will be delayed by that many milliseconds. If the value is a number, the * operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ _setCurrentParameter(data, options = {}) { data = [].concat(data); // MSB // data[0] = parseInt(data[0]); // if (!isNaN(data[0]) && data[0] >= 0 && data[0] <= 127) { this.sendControlChange(0x06, data[0], options); // } else { // throw new RangeError("The msb value must be between 0 and 127."); // } if (data.length < 2) return this; // LSB // data[1] = parseInt(data[1]); // if (!isNaN(data[1]) && data[1] >= 0 && data[1] <= 127) { this.sendControlChange(0x26, data[1], options); // } else { // throw new RangeError("The lsb value must be between 0 and 127."); // } return this; } /** * Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this function: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified registered parameter is invalid. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnDecrement(parameter, options = {}) { if (!Array.isArray(parameter)) parameter = Enumerations.REGISTERED_PARAMETERS[parameter]; if (WebMidi.validation) { if (parameter === undefined) { throw new TypeError("The specified registered parameter is invalid."); } let valid = false; Object.getOwnPropertyNames(Enumerations.REGISTERED_PARAMETERS).forEach(p => { if ( Enumerations.REGISTERED_PARAMETERS[p][0] === parameter[0] && Enumerations.REGISTERED_PARAMETERS[p][1] === parameter[1] ) { valid = true; } }); if (!valid) throw new TypeError("The specified registered parameter is invalid."); } this._selectRegisteredParameter(parameter, options); this.sendControlChange(0x61, 0, options); this._deselectRegisteredParameter(options); return this; } /** * Increments the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this function: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified registered parameter is invalid. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnIncrement(parameter, options = {}) { if (!Array.isArray(parameter)) parameter = Enumerations.REGISTERED_PARAMETERS[parameter]; if (WebMidi.validation) { if (parameter === undefined) { throw new TypeError("The specified registered parameter is invalid."); } let valid = false; Object.getOwnPropertyNames(Enumerations.REGISTERED_PARAMETERS).forEach(p => { if ( Enumerations.REGISTERED_PARAMETERS[p][0] === parameter[0] && Enumerations.REGISTERED_PARAMETERS[p][1] === parameter[1] ) { valid = true; } }); if (!valid) throw new TypeError("The specified registered parameter is invalid."); } this._selectRegisteredParameter(parameter, options); this.sendControlChange(0x60, 0, options); this._deselectRegisteredParameter(options); return this; } /** * Plays a note or an array of notes on the channel. The first parameter is the note to play. It * can be a single value or an array of the following valid values: * * - A [`Note`]{@link Note} object * - A MIDI note number (integer between `0` and `127`) * - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * * The `playNote()` method sends a **note on** MIDI message for all specified notes. If a * `duration` is set in the `options` parameter or in the [`Note`]{@link Note} object's * [`duration`]{@link Note#duration} property, it will also schedule a **note off** message * to end the note after said duration. If no `duration` is set, the note will simply play until * a matching **note off** message is sent with [`stopNote()`]{@link OutputChannel#stopNote} or * [`sendNoteOff()`]{@link OutputChannel#sendNoteOff}. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the durations and velocities defined in the * [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options` * parameter. * * **Note**: per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, * `F-1`, `Db7`), a [`Note`]{@link Note} object or an array of the previous types. When using a * note identifier, the octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI * note number `0`) and the highest note is `G9` (MIDI note number `127`). * * @param {object} [options={}] * * @param {number} [options.duration] A positive decimal number larger than `0` representing the * number of milliseconds to wait before sending a **note off** message. If invalid or left * undefined, only a **note on** message will be sent. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity * value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between * `0` and `127`). This has priority over the `attack` property. An invalid velocity value will * silently trigger the default of 64. * * @param {number} [options.release=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid * velocity value will silently trigger the default of `0.5`. This is only used with the * **note off** event triggered when `options.duration` is set. * * @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0` * and `127`). This has priority over the `release` property. An invalid velocity value will * silently trigger the default of 64. This is only used with the **note off** event triggered * when `options.duration` is set. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ playNote(note, options = {}) { // Send note on and, optionally, note off message (if duration is a positive number) this.sendNoteOn(note, options); const notes = Array.isArray(note) ? note : [note]; for(let note of notes) { if (parseInt(note.duration) > 0) { const noteOffOptions = { time: (Utilities.toTimestamp(options.time) || WebMidi.time) + parseInt(note.duration), release: note.release, rawRelease: note.rawRelease }; this.sendNoteOff(note, noteOffOptions); } else if (parseInt(options.duration) > 0) { const noteOffOptions = { time: (Utilities.toTimestamp(options.time) || WebMidi.time) + parseInt(options.duration), release: options.release, rawRelease: options.rawRelease }; this.sendNoteOff(note, noteOffOptions); } } return this; } /** * Sends a **note off** message for the specified notes on the channel. The first parameter is the * note. It can be a single value or an array of the following valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`]{@link Note} object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the release velocity defined in the * [`Note`]{@link Note} objects has precedence over the one specified via the method's `options` * parameter. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. When using a note name, octave * range must be between -1 and 9. The lowest note is C-1 (MIDI note number 0) and the highest * note is G9 (MIDI note number 127). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNoteOff(note, options = {}) { if (WebMidi.validation) { if ( options.rawRelease != undefined && !(options.rawRelease >= 0 && options.rawRelease <= 127) ) { throw new RangeError("The 'rawRelease' option must be an integer between 0 and 127"); } if (options.release != undefined && !(options.release >= 0 && options.release <= 1)) { throw new RangeError("The 'release' option must be an number between 0 and 1"); } // Legacy compatibility warnings if (options.rawVelocity) { options.rawRelease = options.velocity; console.warn("The 'rawVelocity' option is deprecated. Use 'rawRelease' instead."); } if (options.velocity) { options.release = options.velocity; console.warn("The 'velocity' option is deprecated. Use 'attack' instead."); } } let nVelocity = 64; if (options.rawRelease != undefined) { nVelocity = options.rawRelease; } else { if (!isNaN(options.release)) nVelocity = Math.round(options.release * 127); } // Plot total octave offset const offset = WebMidi.octaveOffset + this.output.octaveOffset + this.octaveOffset; Utilities.buildNoteArray(note, {rawRelease: parseInt(nVelocity)}).forEach(n => { this.send( [ (Enumerations.CHANNEL_MESSAGES.noteoff << 4) + (this.number - 1), n.getOffsetNumber(offset), n.rawRelease, ], {time: Utilities.toTimestamp(options.time)} ); }); return this; } /** * Sends a **note off** message for the specified MIDI note number. The first parameter is the * note to stop. It can be a single value or an array of the following valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ stopNote(note, options = {}) { return this.sendNoteOff(note, options); } /** * Sends a **note on** message for the specified note(s) on the channel. The first parameter is * the note. It can be a single value or an array of the following valid values: * * - A [`Note`]{@link Note} object * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * * When passing a [`Note`]{@link Note}object or a note name, the `octaveOffset` will be applied. * This is not the case when using a note number. In this case, we assume you know exactly which * MIDI note number should be sent out. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the attack velocity defined in the * [`Note`]{@link Note} objects has precedence over the one specified via the method's `options` * parameter. Also, the `duration` is ignored. If you want to also send a **note off** message, * use the [`playNote()`]{@link #playNote} method instead. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0` * and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `64`. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNoteOn(note, options = {}) { if (WebMidi.validation) { if (options.rawAttack != undefined && !(options.rawAttack >= 0 && options.rawAttack <= 127)) { throw new RangeError("The 'rawAttack' option must be an integer between 0 and 127"); } if (options.attack != undefined && !(options.attack >= 0 && options.attack <= 1)) { throw new RangeError("The 'attack' option must be an number between 0 and 1"); } // Legacy compatibility warnings if (options.rawVelocity) { options.rawAttack = options.velocity; options.rawRelease = options.release; console.warn("The 'rawVelocity' option is deprecated. Use 'rawAttack' or 'rawRelease'."); } if (options.velocity) { options.attack = options.velocity; console.warn("The 'velocity' option is deprecated. Use 'attack' instead."); } } let nVelocity = 64; if (options.rawAttack != undefined) { nVelocity = options.rawAttack; } else { if (!isNaN(options.attack)) nVelocity = Math.round(options.attack * 127); } // Plot total octave offset const offset = WebMidi.octaveOffset + this.output.octaveOffset + this.octaveOffset; Utilities.buildNoteArray(note, {rawAttack: nVelocity}).forEach(n => { this.send( [ (Enumerations.CHANNEL_MESSAGES.noteon << 4) + (this.number - 1), n.getOffsetNumber(offset), n.rawAttack ], {time: Utilities.toTimestamp(options.time)} ); }); return this; } /** * Sends a MIDI **channel mode** message. The channel mode message to send can be specified * numerically or by using one of the following common names: * * | Type |Number| Shortcut Method | * | ---------------------|------|-------------------------------------------------------------- | * | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} | * | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} | * | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} | * | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} | * | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} | * | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} | * | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} | * | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} | * * **Note**: as you can see above, to make it easier, all channel mode messages also have a matching * helper method. * * It should be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` may * require a value that's not zero. For that reason, the `value` parameter is optional and * defaults to 0. * * @param {number|string} command The numerical identifier of the channel mode message (integer * between `120` and `127`) or its name as a string. * * @param {number} [value=0] The value to send (integer between `0` - `127`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendChannelMode(command, value = 0, options = {}) { // Normalize command to integer if (typeof command === "string") command = Enumerations.CHANNEL_MODE_MESSAGES[command]; if (WebMidi.validation) { if (command === undefined) { throw new TypeError("Invalid channel mode message name or number."); } if (isNaN(command) || !(command >= 120 && command <= 127)) { throw new TypeError("Invalid channel mode message number."); } if (isNaN(parseInt(value)) || value < 0 || value > 127) { throw new RangeError("Value must be an integer between 0 and 127."); } } this.send( [ (Enumerations.CHANNEL_MESSAGES.controlchange << 4) + (this.number - 1), command, value ], {time: Utilities.toTimestamp(options.time)} ); return this; } /** * Sets OMNI mode to `"on"` or `"off"`. MIDI's OMNI mode causes the instrument to respond to * messages from all channels. * * It should be noted that support for OMNI mode is not as common as it used to be. * * @param [state=true] {boolean} Whether to activate OMNI mode (`true`) or not (`false`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendOmniMode(state, options = {}) { if (state === undefined || state) { this.sendChannelMode("omnimodeon", 0, options); } else { this.sendChannelMode("omnimodeoff", 0, options); } return this; } /** * Sends a MIDI **channel aftertouch** message. For key-specific aftertouch, you should instead * use [`sendKeyAftertouch()`]{@link #sendKeyAftertouch}. * * @param [pressure] {number} The pressure level (between `0` and `1`). If the `rawValue` option * is set to `true`, the pressure can be defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @throws RangeError Invalid channel aftertouch value. */ sendChannelAftertouch(pressure, options = {}) { if (WebMidi.validation) { if (isNaN(parseFloat(pressure))) { throw new RangeError("Invalid channel aftertouch value."); } if (options.rawValue) { if (!(pressure >= 0 && pressure <= 127 && Number.isInteger(pressure))) { throw new RangeError( "Channel aftertouch raw value must be an integer between 0 and 127.") ; } } else { if (!(pressure >= 0 && pressure <= 1)) { throw new RangeError("Channel aftertouch value must be a float between 0 and 1."); } } } // Normalize pressure to integer if (!options.rawValue) pressure = Utilities.fromFloatTo7Bit(pressure); this.send( [ (Enumerations.CHANNEL_MESSAGES.channelaftertouch << 4) + (this.number - 1), Math.round(pressure) ], {time: Utilities.toTimestamp(options.time)} ); return this; } /** * Sends a **master tuning** message. The value is decimal and must be larger than -65 semitones * and smaller than 64 semitones. * * Because of the way the MIDI specification works, the decimal portion of the value will be * encoded with a resolution of 14bit. The integer portion must be between -64 and 63 * inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and * a **Master Fine Tuning** RPN messages. * * @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64) * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The value must be a decimal number between larger than -65 and smaller * than 64. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendMasterTuning(value, options = {}) { // @todo allow passing value as msb/lsb pair (the same as pitch bend range) value = parseFloat(value) || 0.0; if (WebMidi.validation) { if (!(value > -65 && value < 64)) { throw new RangeError( "The value must be a decimal number larger than -65 and smaller than 64." ); } } let coarse = Math.floor(value) + 64; let fine = value - Math.floor(value); // Calculate MSB and LSB for fine adjustment (14bit resolution) fine = Math.round((fine + 1) / 2 * 16383); let msb = (fine >> 7) & 0x7F; let lsb = fine & 0x7F; this.sendRpnValue("channelcoarsetuning", coarse, options); this.sendRpnValue("channelfinetuning", [msb, lsb], options); return this; } /** * Sends a **modulation depth range** message to adjust the depth of the modulation wheel's range. * The range can be specified with the `semitones` parameter, the `cents` parameter or by * specifying both parameters at the same time. * * @param {number} semitones The desired adjustment value in semitones (integer between 0 and * 127). * * @param {number} [cents=0] The desired adjustment value in cents (integer between 0 and 127). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendModulationRange(semitones, cents, options = {}) { // @todo allow passing value as msb/lsb pair (the same as pitch bend range) // when passing a single argument, semitones and cents shoud be combined if (WebMidi.validation) { if (!Number.isInteger(semitones) || !(semitones >= 0 && semitones <= 127)) { throw new RangeError("The semitones value must be an integer between 0 and 127."); } if (!(cents == undefined) && (!Number.isInteger(cents) || !(cents >= 0 && cents <= 127))) { throw new RangeError("If specified, the cents value must be an integer between 0 and 127."); } } // Default value for cents if (!(cents >= 0 && cents <= 127)) cents = 0; this.sendRpnValue("modulationrange", [semitones, cents], options); return this; } /** * Sets a non-registered parameter (NRPN) to the specified value. The NRPN is selected by passing * in a two-position array specifying the values of the two control bytes. The value is specified * by passing in a single integer (most cases) or an array of two integers. * * NRPNs are not standardized in any way. Each manufacturer is free to implement them any way * they see fit. For example, according to the Roland GS specification, you can control the * **vibrato rate** using NRPN (1, 8). Therefore, to set the **vibrato rate** value to **123** you * would use: * * ```js * WebMidi.outputs[0].channels[0].sendNrpnValue([1, 8], 123); * ``` * * In some rarer cases, you need to send two values with your NRPN messages. In such cases, you * would use a 2-position array. For example, for its **ClockBPM** parameter (2, 63), Novation * uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the * value to send was 10, you could use: * * ```js * WebMidi.outputs[0].channels[0].sendNrpnValue([2, 63], [0, 10]); * ``` * * For further implementation details, refer to the manufacturer's documentation. * * @param nrpn {number[]} A two-position array specifying the two control bytes (0x63, * 0x62) that identify the non-registered parameter. * * @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2 * specifying the desired data. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The control value must be between 0 and 127. * @throws {RangeError} The msb value must be between 0 and 127 * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNrpnValue(nrpn, data, options = {}) { data = [].concat(data); if (WebMidi.validation) { if (!Array.isArray(nrpn) || !Number.isInteger(nrpn[0]) || !Number.isInteger(nrpn[1])) { throw new TypeError("The specified NRPN is invalid."); } if (!(nrpn[0] >= 0 && nrpn[0] <= 127)) { throw new RangeError("The first byte of the NRPN must be between 0 and 127."); } if (!(nrpn[1] >= 0 && nrpn[1] <= 127)) { throw new RangeError("The second byte of the NRPN must be between 0 and 127."); } data.forEach(value => { if (!(value >= 0 && value <= 127)) { throw new RangeError("The data bytes of the NRPN must be between 0 and 127."); } }); } this._selectNonRegisteredParameter(nrpn, options); this._setCurrentParameter(data, options); this._deselectNonRegisteredParameter(options); return this; } /** * Sends a MIDI **pitch bend** message at the scheduled time. The resulting bend is relative to * the pitch bend range that has been defined. The range can be set with * [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch * bend range has been set to 12 semitones, using a bend value of -1 will bend the note 1 octave * below its nominal value. * * @param {number|number[]} [value] The intensity of the bend (between -1.0 and 1.0). A value of * zero means no bend. If the `rawValue` option is set to `true`, the intensity of the bend can be * defined by either using a single integer between 0 and 127 (MSB) or an array of two integers * between 0 and 127 representing, respectively, the MSB (most significant byte) and the LSB * (least significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value * lower than `64` bends downwards while a value higher than `64` bends upwards. The LSB is * expressed in cents (1/100 of a semitone). An LSB of `64` also means no bend. * * @param {Object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered as a float between -1.0 and 1.0 (default) or as raw integer between 0 and 127 (or * an array of 2 integers if using both MSB and LSB). * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPitchBend(value, options = {}) { // @todo standardize the way msb/lsb are passed in if (WebMidi.validation) { if (options.rawValue && Array.isArray(value)) { if (!(value[0] >= 0 && value[0] <= 127)) { throw new RangeError("The pitch bend MSB must be an integer between 0 and 127."); } if (!(value[1] >= 0 && value[1] <= 127)) { throw new RangeError("The pitch bend LSB must be an integer between 0 and 127."); } } else if (options.rawValue && !Array.isArray(value)) { if (!(value >= 0 && value <= 127)) { throw new RangeError("The pitch bend MSB must be an integer between 0 and 127."); } } else { if (isNaN(value) || value === null) { throw new RangeError("Invalid pitch bend value."); } if (!(value >= -1 && value <= 1)) { throw new RangeError("The pitch bend value must be a float between -1 and 1."); } } } let msb = 0; let lsb = 0; // Calculate MSB and LSB for both scenarios if (options.rawValue && Array.isArray(value)) { msb = value[0]; lsb = value[1]; } else if (options.rawValue && !Array.isArray(value)) { msb = value; } else { const result = Utilities.fromFloatToMsbLsb((value + 1) / 2); // b/c value is -1 to 1 msb = result.msb; lsb = result.lsb; } this.send( [ (Enumerations.CHANNEL_MESSAGES.pitchbend << 4) + (this.number - 1), lsb, msb ], {time: Utilities.toTimestamp(options.time)} ); return this; } /** * Sends a **pitch bend range** message at the scheduled time to adjust the range used by the * pitch bend lever. The range is specified by using the `semitones` and `cents` parameters. For * example, setting the `semitones` parameter to `12` means that the pitch bend range will be 12 * semitones above and below the nominal pitch. * * @param semitones {number} The desired adjustment value in semitones (between 0 and 127). While * nothing imposes that in the specification, it is very common for manufacturers to limit the * range to 2 octaves (-12 semitones to 12 semitones). * * @param [cents=0] {number} The desired adjustment value in cents (integer between 0-127). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The semitones value must be an integer between 0 and 127. * @throws {RangeError} The cents value must be an integer between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPitchBendRange(semitones, cents, options = {}) { // @todo use single value as parameter or pair of msb/lsb if (WebMidi.validation) { if (!Number.isInteger(semitones) || !(semitones >= 0 && semitones <= 127)) { throw new RangeError("The semitones value must be an integer between 0 and 127."); } if (!Number.isInteger(cents) || !(cents >= 0 && cents <= 127)) { throw new RangeError("The cents value must be an integer between 0 and 127."); } } this.sendRpnValue("pitchbendrange", [semitones, cents], options); return this; } /** * Sends a MIDI **program change** message at the scheduled time. * * @param [program=1] {number} The MIDI patch (program) number (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater * than 0xFF. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * */ sendProgramChange(program, options = {}) { program = parseInt(program) || 0; if (WebMidi.validation) { if (!(program >= 0 && program <= 127)) { throw new RangeError("The program number must be between 0 and 127."); } } this.send( [ (Enumerations.CHANNEL_MESSAGES.programchange << 4) + (this.number - 1), program ], {time: Utilities.toTimestamp(options.time)} ); return this; } /** * Sets the specified MIDI registered parameter to the desired value. The value is defined with * up to two bytes of data (msb, lsb) that each can go from 0 to 127. * * MIDI * [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * extend the original list of control change messages. The MIDI 1.0 specification lists only a * limited number of them: * * | Numbers | Function | * |--------------|--------------------------| * | (0x00, 0x00) | `pitchbendrange` | * | (0x00, 0x01) | `channelfinetuning` | * | (0x00, 0x02) | `channelcoarsetuning` | * | (0x00, 0x03) | `tuningprogram` | * | (0x00, 0x04) | `tuningbank` | * | (0x00, 0x05) | `modulationrange` | * | (0x3D, 0x00) | `azimuthangle` | * | (0x3D, 0x01) | `elevationangle` | * | (0x3D, 0x02) | `gain` | * | (0x3D, 0x03) | `distanceratio` | * | (0x3D, 0x04) | `maximumdistance` | * | (0x3D, 0x05) | `maximumdistancegain` | * | (0x3D, 0x06) | `referencedistanceratio` | * | (0x3D, 0x07) | `panspreadangle` | * | (0x3D, 0x08) | `rollangle` | * * Note that the **Tuning Program** and **Tuning Bank** parameters are part of the *MIDI Tuning * Standard*, which is not widely implemented. * * @param rpn {string|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the * registered parameter. * * @param [data=[]] {number|number[]} An single integer or an array of integers with a maximum * length of 2 specifying the desired data. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnValue(rpn, data, options = {}) { if (!Array.isArray(rpn)) rpn = Enumerations.REGISTERED_PARAMETERS[rpn]; if (WebMidi.validation) { if (!Number.isInteger(rpn[0]) || !Number.isInteger(rpn[1])) { throw new TypeError("The specified NRPN is invalid."); } if (!(rpn[0] >= 0 && rpn[0] <= 127)) { throw new RangeError("The first byte of the RPN must be between 0 and 127."); } if (!(rpn[1] >= 0 && rpn[1] <= 127)) { throw new RangeError("The second byte of the RPN must be between 0 and 127."); } [].concat(data).forEach(value => { if (!(value >= 0 && value <= 127)) { throw new RangeError("The data bytes of the RPN must be between 0 and 127."); } }); } this._selectRegisteredParameter(rpn, options); this._setCurrentParameter(data, options); this._deselectRegisteredParameter(options); return this; } /** * Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning bank (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The bank value must be between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendTuningBank(value, options = {}) { if (WebMidi.validation) { if (!Number.isInteger(value) || !(value >= 0 && value <= 127)) { throw new RangeError("The tuning bank number must be between 0 and 127."); } } this.sendRpnValue("tuningbank", value, options); return this; } /** * Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning program (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The program value must be between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendTuningProgram(value, options = {}) { if (WebMidi.validation) { if (!Number.isInteger(value) || !(value >= 0 && value <= 127)) { throw new RangeError("The tuning program number must be between 0 and 127."); } } this.sendRpnValue("tuningprogram", value, options); return this; } /** * Turns local control on or off. Local control is usually enabled by default. If you disable it, * the instrument will no longer trigger its own sounds. It will only send the MIDI messages to * its out port. * * @param [state=false] {boolean} Whether to activate local control (`true`) or disable it * (`false`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendLocalControl(state, options = {}) { if (state) { return this.sendChannelMode("localcontrol", 127, options); } else { return this.sendChannelMode("localcontrol", 0, options); } } /** * Sends an **all notes off** channel mode message. This will make all currently playing notes * fade out just as if their key had been released. This is different from the * [`sendAllSoundOff()`]{@link #sendAllSoundOff} method which mutes all sounds immediately. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendAllNotesOff(options = {}) { return this.sendChannelMode("allnotesoff", 0, options); } /** * Sends an **all sound off** channel mode message. This will silence all sounds playing on that * channel but will not prevent new sounds from being triggered. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendAllSoundOff(options = {}) { return this.sendChannelMode("allsoundoff", 0, options); } /** * Sends a **reset all controllers** channel mode message. This resets all controllers, such as * the pitch bend, to their default value. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendResetAllControllers(options = {}) { return this.sendChannelMode("resetallcontrollers", 0, options); } /** * Sets the polyphonic mode. In `"poly"` mode (usually the default), multiple notes can be played * and heard at the same time. In `"mono"` mode, only one note will be heard at once even if * multiple notes are being played. * * @param {string} [mode=poly] The mode to use: `"mono"` or `"poly"`. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPolyphonicMode(mode, options = {}) { if (mode === "mono") { return this.sendChannelMode("monomodeon", 0, options); } else { return this.sendChannelMode("polymodeon", 0, options); } } /** * An integer to offset the reported octave of outgoing note-specific messages (`noteon`, * `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th * octave (C4). * * Note that this value is combined with the global offset value defined in * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) and with the parent value defined in * [`Output.octaveOffset`]{@link Output#octaveOffset}. * * @type {number} * * @since 3.0 */ get octaveOffset() { return this._octaveOffset; } set octaveOffset(value) { if (this.validation) { value = parseInt(value); if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer."); } this._octaveOffset = value; } /** * The parent [`Output`]{@link Output} this channel belongs to. * @type {Output} * @since 3.0 */ get output() { return this._output; } /** * This channel's MIDI number (`1` - `16`). * @type {number} * @since 3.0 */ get number() { return this._number; } } ================================================ FILE: src/Utilities.js ================================================ import {Note} from "./Note.js"; import {WebMidi} from "./WebMidi.js"; import {Enumerations} from "./Enumerations.js"; /** * The `Utilities` class contains general-purpose utility methods. All methods are static and * should be called using the class name. For example: `Utilities.getNoteDetails("C4")`. * * @license Apache-2.0 * @since 3.0.0 */ export class Utilities { /** * Returns a MIDI note number matching the identifier passed in the form of a string. The * identifier must include the octave number. The identifier also optionally include a sharp (#), * a double sharp (##), a flat (b) or a double flat (bb) symbol. For example, these are all valid * identifiers: C5, G4, D#-1, F0, Gb7, Eb-1, Abb4, B##6, etc. * * When converting note identifiers to numbers, C4 is considered to be middle C (MIDI note number * 60) as per the scientific pitch notation standard. * * The resulting note number can be offset by using the `octaveOffset` parameter. * * @param identifier {string} The identifier in the form of a letter, followed by an optional "#", * "##", "b" or "bb" followed by the octave number. For exemple: C5, G4, D#-1, F0, Gb7, Eb-1, * Abb4, B##6, etc. * * @param {number} [octaveOffset=0] A integer to offset the octave by. * * @returns {number} The MIDI note number (an integer between 0 and 127). * * @throws RangeError Invalid 'octaveOffset' value * * @throws TypeError Invalid note identifier * * @license Apache-2.0 * @since 3.0.0 * @static */ static toNoteNumber(identifier, octaveOffset = 0) { // Validation octaveOffset = octaveOffset == undefined ? 0 : parseInt(octaveOffset); if (isNaN(octaveOffset)) throw new RangeError("Invalid 'octaveOffset' value"); if (typeof identifier !== "string") identifier = ""; const fragments = this.getNoteDetails(identifier); if (!fragments) throw new TypeError("Invalid note identifier"); const notes = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }; let result = (fragments.octave + 1 + octaveOffset) * 12; result += notes[fragments.name]; if (fragments.accidental) { if (fragments.accidental.startsWith("b")) { result -= fragments.accidental.length; } else { result += fragments.accidental.length; } } if (result < 0 || result > 127) throw new RangeError("Invalid octaveOffset value"); return result; } /** * Given a proper note identifier (`C#4`, `Gb-1`, etc.) or a valid MIDI note number (0-127), this * method returns an object containing broken down details about the specified note (uppercase * letter, accidental and octave). * * When a number is specified, the translation to note is done using a value of 60 for middle C * (C4 = middle C). * * @param value {string|number} A note identifier A atring ("C#4", "Gb-1", etc.) or a MIDI note * number (0-127). * * @returns {{accidental: string, identifier: string, name: string, octave: number }} * * @throws TypeError Invalid note identifier * * @since 3.0.0 * @static */ static getNoteDetails(value) { if (Number.isInteger(value)) value = this.toNoteIdentifier(value); const matches = value.match(/^([CDEFGAB])(#{0,2}|b{0,2})(-?\d+)$/i); if (!matches) throw new TypeError("Invalid note identifier"); const name = matches[1].toUpperCase(); const octave = parseInt(matches[3]); let accidental = matches[2].toLowerCase(); accidental = accidental === "" ? undefined : accidental; const fragments = { accidental: accidental, identifier: name + (accidental || "") + octave, name: name, octave: octave }; return fragments; } /** * Returns a sanitized array of valid MIDI channel numbers (1-16). The parameter should be a * single integer or an array of integers. * * For backwards-compatibility, passing `undefined` as a parameter to this method results in all * channels being returned (1-16). Otherwise, parameters that cannot successfully be parsed to * integers between 1 and 16 are silently ignored. * * @param [channel] {number|number[]} An integer or an array of integers to parse as channel * numbers. * * @returns {number[]} An array of 0 or more valid MIDI channel numbers. * * @since 3.0.0 * @static */ static sanitizeChannels(channel) { let channels; if (WebMidi.validation) { if (channel === "all") { // backwards-compatibility channels = ["all"]; } else if (channel === "none") { // backwards-compatibility return []; } } if (!Array.isArray(channel)) { channels = [channel]; } else { channels = channel; } // In order to preserve backwards-compatibility, we let this assignment as it is. if (channels.indexOf("all") > -1) { channels = Enumerations.MIDI_CHANNEL_NUMBERS; } return channels .map(function(ch) { return parseInt(ch); }) .filter(function(ch) { return (ch >= 1 && ch <= 16); }); } /** * Returns a valid timestamp, relative to the navigation start of the document, derived from the * `time` parameter. If the parameter is a string starting with the "+" sign and followed by a * number, the resulting timestamp will be the sum of the current timestamp plus that number. If * the parameter is a positive number, it will be returned as is. Otherwise, false will be * returned. * * @param [time] {number|string} The time string (e.g. `"+2000"`) or number to parse * @return {number|false} A positive number or `false` (if the time cannot be converted) * * @since 3.0.0 * @static */ static toTimestamp(time) { let value = false; const parsed = parseFloat(time); if (isNaN(parsed)) return false; if (typeof time === "string" && time.substring(0, 1) === "+") { if (parsed >= 0) value = WebMidi.time + parsed; } else { if (parsed >= 0) value = parsed; } return value; } /** * Returns a valid MIDI note number (0-127) given the specified input. The input usually is a * string containing a note identifier (`"C3"`, `"F#4"`, `"D-2"`, `"G8"`, etc.). If an integer * between 0 and 127 is passed, it will simply be returned as is (for convenience). Other strings * will be parsed for integer value, if possible. * * If the input is an identifier, the resulting note number is offset by the `octaveOffset` * parameter. For example, if you pass in "C4" (note number 60) and the `octaveOffset` value is * -2, the resulting MIDI note number will be 36. * * @param input {string|number} A string or number to extract the MIDI note number from. * @param octaveOffset {number} An integer to offset the octave by * * @returns {number|false} A valid MIDI note number (0-127) or `false` if the input could not * successfully be parsed to a note number. * * @since 3.0.0 * @static */ static guessNoteNumber(input, octaveOffset) { // Validate and, if necessary, assign default octaveOffset = parseInt(octaveOffset) || 0; let output = false; // Check input type if (Number.isInteger(input) && input >= 0 && input <= 127) { // uint output = parseInt(input); } else if (parseInt(input) >= 0 && parseInt(input) <= 127) { // float or uint as string output = parseInt(input); } else if (typeof input === "string" || input instanceof String) { // string try { output = this.toNoteNumber(input.trim(), octaveOffset); } catch (e) { return false; } } return output; } /** * Returns an identifier string representing a note name (with optional accidental) followed by an * octave number. The octave can be offset by using the `octaveOffset` parameter. * * @param {number} number The MIDI note number to convert to a note identifier * @param {number} octaveOffset An offset to apply to the resulting octave * * @returns {string} * * @throws RangeError Invalid note number * @throws RangeError Invalid octaveOffset value * * @since 3.0.0 * @static */ static toNoteIdentifier(number, octaveOffset) { number = parseInt(number); if (isNaN(number) || number < 0 || number > 127) throw new RangeError("Invalid note number"); octaveOffset = octaveOffset == undefined ? 0 : parseInt(octaveOffset); if (isNaN(octaveOffset)) throw new RangeError("Invalid octaveOffset value"); const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const octave = Math.floor(number / 12 - 1) + octaveOffset; return notes[number % 12] + octave.toString(); } /** * Converts the `input` parameter to a valid [`Note`]{@link Note} object. The input usually is an * unsigned integer (0-127) or a note identifier (`"C4"`, `"G#5"`, etc.). If the input is a * [`Note`]{@link Note} object, it will be returned as is. * * If the input is a note number or identifier, it is possible to specify options by providing the * `options` parameter. * * @param [input] {number|string|Note} * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should * be explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only * used when the input value is a note identifier.** * * @returns {Note} * * @throws TypeError The input could not be parsed to a note * * @since version 3.0.0 * @static */ static buildNote(input, options= {}) { options.octaveOffset = parseInt(options.octaveOffset) || 0; // If it's already a Note, we're done if (input instanceof Note) return input; let number = this.guessNoteNumber(input, options.octaveOffset); if (number === false) { // We use a comparison b/c the note can be 0 (which equates to false) throw new TypeError(`The input could not be parsed as a note (${input})`); } // If we got here, we have a proper note number. Before creating the new note, we strip out // 'octaveOffset' because it has already been factored in when calling guessNoteNumber(). options.octaveOffset = undefined; return new Note(number, options); } /** * Converts an input value, which can be an unsigned integer (0-127), a note identifier, a * [`Note`]{@link Note} object or an array of the previous types, to an array of * [`Note`]{@link Note} objects. * * [`Note`]{@link Note} objects are returned as is. For note numbers and identifiers, a * [`Note`]{@link Note} object is created with the options specified. An error will be thrown when * encountering invalid input. * * Note: if both the `attack` and `rawAttack` options are specified, the later has priority. The * same goes for `release` and `rawRelease`. * * @param [notes] {number|string|Note|number[]|string[]|Note[]} * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should * be explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only * used when the input value is a note identifier.** * * @returns {Note[]} * * @throws TypeError An element could not be parsed as a note. * * @since 3.0.0 * @static */ static buildNoteArray(notes, options = {}) { let result = []; if (!Array.isArray(notes)) notes = [notes]; notes.forEach(note => { result.push(this.buildNote(note, options)); }); return result; } /** * Returns a number between 0 and 1 representing the ratio of the input value divided by 127 (7 * bit). The returned value is restricted between 0 and 1 even if the input is greater than 127 or * smaller than 0. * * Passing `Infinity` will return `1` and passing `-Infinity` will return `0`. Otherwise, when the * input value cannot be converted to an integer, the method returns 0. * * @param value {number} A positive integer between 0 and 127 (inclusive) * @returns {number} A number between 0 and 1 (inclusive) * @static */ static from7bitToFloat(value) { if (value === Infinity) value = 127; value = parseInt(value) || 0; return Math.min(Math.max(value / 127, 0), 1); } /** * Returns an integer between 0 and 127 which is the result of multiplying the input value by * 127. The input value should be a number between 0 and 1 (inclusively). The returned value is * restricted between 0 and 127 even if the input is greater than 1 or smaller than 0. * * Passing `Infinity` will return `127` and passing `-Infinity` will return `0`. Otherwise, when * the input value cannot be converted to a number, the method returns 0. * * @param value {number} A positive float between 0 and 1 (inclusive) * @returns {number} A number between 0 and 127 (inclusive) * @static */ static fromFloatTo7Bit(value) { if (value === Infinity) value = 1; value = parseFloat(value) || 0; return Math.min(Math.max(Math.round(value * 127), 0), 127); } /** * Combines and converts MSB and LSB values (0-127) to a float between 0 and 1. The returned value * is within between 0 and 1 even if the result is greater than 1 or smaller than 0. * * @param msb {number} The most significant byte as a integer between 0 and 127. * @param [lsb=0] {number} The least significant byte as a integer between 0 and 127. * @returns {number} A float between 0 and 1. */ static fromMsbLsbToFloat(msb, lsb = 0) { if (WebMidi.validation) { msb = Math.min(Math.max(parseInt(msb) || 0, 0), 127); lsb = Math.min(Math.max(parseInt(lsb) || 0, 0), 127); } const value = ((msb << 7) + lsb) / 16383; return Math.min(Math.max(value, 0), 1); } /** * Extracts 7bit MSB and LSB values from the supplied float. * * @param value {number} A float between 0 and 1 * @returns {{lsb: number, msb: number}} */ static fromFloatToMsbLsb(value) { if (WebMidi.validation) { value = Math.min(Math.max(parseFloat(value) || 0, 0), 1); } const multiplied = Math.round(value * 16383); return { msb: multiplied >> 7, lsb: multiplied & 0x7F }; } /** * Returns the supplied MIDI note number offset by the requested octave and semitone values. If * the calculated value is less than 0, 0 will be returned. If the calculated value is more than * 127, 127 will be returned. If an invalid offset value is supplied, 0 will be used. * * @param number {number} The MIDI note to offset as an integer between 0 and 127. * @param octaveOffset {number} An integer to offset the note by (in octave) * @param octaveOffset {number} An integer to offset the note by (in semitones) * @returns {number} An integer between 0 and 127 * * @throws {Error} Invalid note number * @static */ static offsetNumber(number, octaveOffset = 0, semitoneOffset = 0) { if (WebMidi.validation) { number = parseInt(number); if (isNaN(number)) throw new Error("Invalid note number"); octaveOffset = parseInt(octaveOffset) || 0; semitoneOffset = parseInt(semitoneOffset) || 0; } return Math.min(Math.max(number + (octaveOffset * 12) + semitoneOffset, 0), 127); } /** * Returns the name of the first property of the supplied object whose value is equal to the one * supplied. If nothing is found, `undefined` is returned. * * @param object {object} The object to look for the property in. * @param value {*} Any value that can be expected to be found in the object's properties. * @returns {string|undefined} The name of the matching property or `undefined` if nothing is * found. * @static */ static getPropertyByValue(object, value) { return Object.keys(object).find(key => object[key] === value); } /** * Returns the name of a control change message matching the specified number (0-127). Some valid * control change numbers do not have a specific name or purpose assigned in the MIDI * [spec](https://midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2). * In these cases, the method returns `controllerXXX` (where XXX is the number). * * @param {number} number An integer (0-127) representing the control change message * @returns {string|undefined} The matching control change name or `undefined` if no match was * found. * * @static */ static getCcNameByNumber(number) { if (WebMidi.validation) { number = parseInt(number); if (!(number >= 0 && number <= 127)) return undefined; } return Enumerations.CONTROL_CHANGE_MESSAGES[number].name; } /** * Returns the number of a control change message matching the specified name. * * @param {string} name A string representing the control change message * @returns {string|undefined} The matching control change number or `undefined` if no match was * found. * * @since 3.1 * @static */ static getCcNumberByName(name) { let message = Enumerations.CONTROL_CHANGE_MESSAGES.find(element => element.name === name); if (message) { return message.number; } else { // Legacy (remove in v4) return Enumerations.MIDI_CONTROL_CHANGE_MESSAGES[name]; } } /** * Returns the channel mode name matching the specified number. If no match is found, the function * returns `false`. * * @param {number} number An integer representing the channel mode message (120-127) * @returns {string|false} The name of the matching channel mode or `false` if no match could be * found. * * @since 2.0.0 */ static getChannelModeByNumber(number) { if ( !(number >= 120 && number <= 127) ) return false; for (let cm in Enumerations.CHANNEL_MODE_MESSAGES) { if ( Enumerations.CHANNEL_MODE_MESSAGES.hasOwnProperty(cm) && number === Enumerations.CHANNEL_MODE_MESSAGES[cm] ) { return cm; } } return false; } /** * Indicates whether the execution environment is Node.js (`true`) or not (`false`) * @type {boolean} */ static get isNode() { return typeof process !== "undefined" && process.versions != null && process.versions.node != null; } /** * Indicates whether the execution environment is a browser (`true`) or not (`false`) * @type {boolean} */ static get isBrowser() { return typeof window !== "undefined" && typeof window.document !== "undefined"; } } ================================================ FILE: src/WebMidi.js ================================================ import {EventEmitter} from "../node_modules/djipevents/src/djipevents.js"; import {Input} from "./Input.js"; import {Output} from "./Output.js"; import {Utilities} from "./Utilities.js"; import {Enumerations} from "./Enumerations.js"; /*START-CJS*/ // This code will only be included in the CJS version (CommonJS). /* coud we use this instead of eval(): let jzz = await Object.getPrototypeOf(async function() {}).constructor(` let jzz = await import("jzz"); return jzz.default; `)(); */ // If this code is executed by Node.js then we must import the `jzz` module. I import it in this // convoluted way to prevent Webpack from automatically bundling it in browser bundles where it // isn't needed. if (Utilities.isNode) { // Some environments may have both Node.js and browser runtimes (Electron, NW.js, React Native, // etc.) so we also check for the presence of the window.navigator property. try { window.navigator; } catch (err) { let jzz; eval('jzz = require("jzz")'); if (!global.navigator) global.navigator = {}; // for Node.js prior to v21 Object.assign(global.navigator, jzz); } // The `performance` module appeared in Node.js v8.5.0 but has started to be automatically // imported only in v16+. try { performance; } catch (err) { let performance; eval('performance = require("perf_hooks").performance'); global.performance = performance; } } /*END-CJS*/ /** * The `WebMidi` object makes it easier to work with the low-level Web MIDI API. Basically, it * simplifies sending outgoing MIDI messages and reacting to incoming MIDI messages. * * When using the WebMidi.js library, you should know that the `WebMidi` class has already been * instantiated. You cannot instantiate it yourself. If you use the **IIFE** version, you should * simply use the global object called `WebMidi`. If you use the **CJS** (CommonJS) or **ESM** (ES6 * module) version, you get an already-instantiated object when you import the module. * * @fires WebMidi#connected * @fires WebMidi#disabled * @fires WebMidi#disconnected * @fires WebMidi#enabled * @fires WebMidi#error * @fires WebMidi#midiaccessgranted * @fires WebMidi#portschanged * * @extends EventEmitter * @license Apache-2.0 */ class WebMidi extends EventEmitter { /** * The WebMidi class is a singleton and you cannot instantiate it directly. It has already been * instantiated for you. */ constructor() { super(); /** * Object containing system-wide default values that can be changed to customize how the library * works. * * @type {object} * * @property {object} defaults.note - Default values relating to note * @property {number} defaults.note.attack - A number between 0 and 127 representing the * default attack velocity of notes. Initial value is 64. * @property {number} defaults.note.release - A number between 0 and 127 representing the * default release velocity of notes. Initial value is 64. * @property {number} defaults.note.duration - A number representing the default duration of * notes (in seconds). Initial value is Infinity. */ this.defaults = { note: { attack: Utilities.from7bitToFloat(64), release: Utilities.from7bitToFloat(64), duration: Infinity } }; /** * The [`MIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) * instance used to talk to the lower-level Web MIDI API. This should not be used directly * unless you know what you are doing. * * @type {MIDIAccess} * @readonly */ this.interface = null; /** * Indicates whether argument validation and backwards-compatibility checks are performed * throughout the WebMidi.js library for object methods and property setters. * * This is an advanced setting that should be used carefully. Setting `validation` to `false` * improves performance but should only be done once the project has been thoroughly tested with * `validation` turned on. * * @type {boolean} */ this.validation = true; /** * Array of all (Input) objects * @type {Input[]} * @private */ this._inputs = []; /** * Array of disconnected [`Input`](Input) objects. This is used when inputs are plugged back in * to retain their previous state. * @type {Input[]} * @private */ this._disconnectedInputs = []; /** * Array of all [`Output`](Output) objects * @type {Output[]} * @private */ this._outputs = []; /** * Array of disconnected [`Output`](Output) objects. This is used when outputs are plugged back * in to retain their previous state. * @type {Output[]} * @private */ this._disconnectedOutputs = []; /** * Array of statechange events to process. These events must be parsed synchronously so they do * not override each other. * * @type {string[]} * @private */ this._stateChangeQueue = []; /** * @type {number} * @private */ this._octaveOffset = 0; } /** * Checks if the Web MIDI API is available in the current environment and then tries to connect to * the host's MIDI subsystem. This is an asynchronous operation and it causes a security prompt to * be displayed to the user. * * To enable the use of MIDI system exclusive messages, the `sysex` option should be set to * `true`. However, under some environments (e.g. Jazz-Plugin), the `sysex` option is ignored * and system exclusive messages are always enabled. You can check the * [`sysexEnabled`](#sysexEnabled) property to confirm. * * To enable access to software synthesizers available on the host, you would set the `software` * option to `true`. However, this option is only there to future-proof the library as support for * software synths has not yet been implemented in any browser (as of September 2021). * * By the way, if you call the [`enable()`](#enable) method while WebMidi.js is already enabled, * the callback function will be executed (if any), the promise will resolve but the events * ([`"midiaccessgranted"`](#event:midiaccessgranted), [`"connected"`](#event:connected) and * [`"enabled"`](#event:enabled)) will not be fired. * * There are 3 ways to execute code after `WebMidi` has been enabled: * * - Pass a callback function in the `options` * - Listen to the [`"enabled"`](#event:enabled) event * - Wait for the promise to resolve * * In order, this is what happens towards the end of the enabling process: * * 1. [`"midiaccessgranted"`](#event:midiaccessgranted) event is triggered once the user has * granted access to use MIDI. * 2. [`"connected"`](#event:connected) events are triggered (for each available input and output) * 3. [`"enabled"`](#event:enabled) event is triggered when WebMidi.js is fully ready * 4. specified callback (if any) is executed * 5. promise is resolved and fulfilled with the `WebMidi` object. * * **Important note**: starting with Chrome v77, a page using Web MIDI API must be hosted on a * secure origin (`https://`, `localhost` or `file:///`) and the user will always be prompted to * authorize the operation (no matter if the `sysex` option is `true` or not). * * ##### Example * ```js * // Enabling WebMidi and using the promise * WebMidi.enable().then(() => { * console.log("WebMidi.js has been enabled!"); * }) * ``` * * @param [options] {object} * * @param [options.callback] {function} A function to execute once the operation completes. This * function will receive an `Error` object if enabling the Web MIDI API failed. * * @param [options.sysex=false] {boolean} Whether to enable MIDI system exclusive messages or not. * * @param [options.validation=true] {boolean} Whether to enable library-wide validation of method * arguments and setter values. This is an advanced setting that should be used carefully. Setting * [`validation`](#validation) to `false` improves performance but should only be done once the * project has been thoroughly tested with [`validation`](#validation) turned on. * * @param [options.software=false] {boolean} Whether to request access to software synthesizers on * the host system. This is part of the spec but has not yet been implemented by most browsers as * of April 2020. * * @param [options.requestMIDIAccessFunction] {function} A custom function to use to return * the MIDIAccess object. This is useful if you want to use a polyfill for the Web MIDI API * or if you want to use a custom implementation of the Web MIDI API - probably for testing * purposes. * * @async * * @returns {Promise.} The promise is fulfilled with the `WebMidi` object for * chainability * * @throws {Error} The Web MIDI API is not supported in your environment. * @throws {Error} Jazz-Plugin must be installed to use WebMIDIAPIShim. */ async enable(options = {}, legacy = false) { /*START-ESM*/ // This block is stripped out of the IIFE and CJS versions where it isn't needed. // If this code is executed by Node.js in "module" mode (when "type": "module" is used in the // package.json file), then we must import the `jzz` module. I import it in this convoluted way // to prevent Webpack from automatically bundling it in browser bundles where it isn't needed. if (Utilities.isNode) { // Some environments may have both Node.js and browser runtimes (Electron, NW.js, React // Native, etc.) so we also check for the presence of the window.navigator property. try { window.navigator; } catch (err) { let jzz = await Object.getPrototypeOf(async function() {}).constructor(` let jzz = await import("jzz"); return jzz.default; `)(); if (!global.navigator) global.navigator = {}; // for Node.js prior to v21 Object.assign(global.navigator, jzz); } // The `performance` module appeared in Node.js v8.5.0 but has started to be automatically // imported only in v16+. try { performance; } catch (err) { global.performance = await Object.getPrototypeOf(async function() {}).constructor(` let perf_hooks = await import("perf_hooks"); return perf_hooks.performance; `)(); } } /*END-ESM*/ this.validation = (options.validation !== false); if (this.validation) { // Backwards-compatibility. Previous syntax was: enable(callback, sysex) if (typeof options === "function") options = {callback: options, sysex: legacy}; if (legacy) options.sysex = true; } // If already enabled, trigger callback and resolve promise but do not dispatch events. if (this.enabled) { if (typeof options.callback === "function") options.callback(); return Promise.resolve(); } /** * Event emitted when an error occurs trying to enable `WebMidi` * * @event WebMidi#error * @type {object} * @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in * milliseconds since the navigation start of the document). * @property {WebMidi} target The object that triggered the event * @property {string} type `error` * @property {*} error Actual error that occurred */ const errorEvent = { timestamp: this.time, target: this, type: "error", error: undefined }; /** * Event emitted once the MIDI interface has been successfully created (which implies user has * granted access to MIDI). * * @event WebMidi#midiaccessgranted * @type {object} * @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds * since the navigation start of the document). * @property {WebMidi} target The object that triggered the event * @property {string} type `midiaccessgranted` */ const midiAccessGrantedEvent = { timestamp: this.time, target: this, type: "midiaccessgranted" }; /** * Event emitted once `WebMidi` has been fully enabled * * @event WebMidi#enabled * @type {object} * @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds * since the navigation start of the document). * @property {WebMidi} target The object that triggered the event * @property {string} type `"enabled"` */ const enabledEvent = { timestamp: this.time, target: this, type: "enabled" }; // Request MIDI access (this is where the prompt will appear) try { if (typeof options.requestMIDIAccessFunction === "function") { this.interface = await options.requestMIDIAccessFunction( {sysex: options.sysex, software: options.software} ); } else { this.interface = await navigator.requestMIDIAccess( {sysex: options.sysex, software: options.software} ); } } catch(err) { errorEvent.error = err; this.emit("error", errorEvent); if (typeof options.callback === "function") options.callback(err); return Promise.reject(err); } // Now that the Web MIDI API interface has been created, we trigger the 'midiaccessgranted' // event. This allows the developer an occasion to assign listeners on 'connected' events. this.emit("midiaccessgranted", midiAccessGrantedEvent); // We setup the state change listener before creating the ports so that it properly catches the // the ports' `connected` events this.interface.onstatechange = this._onInterfaceStateChange.bind(this); // Update inputs and outputs (this is where `Input` and `Output` objects are created). try { await this._updateInputsAndOutputs(); } catch (err) { errorEvent.error = err; this.emit("error", errorEvent); if (typeof options.callback === "function") options.callback(err); return Promise.reject(err); } // If we make it here, the ports have been successfully created, so we trigger the 'enabled' // event. this.emit("enabled", enabledEvent); // Execute the callback (if any) and resolve the promise with 'this' (for chainability) if (typeof options.callback === "function") options.callback(); return Promise.resolve(this); } /** * Completely disables **WebMidi.js** by unlinking the MIDI subsystem's interface and closing all * [`Input`](Input) and [`Output`](Output) objects that may have been opened. This also means that * listeners added to [`Input`](Input) objects, [`Output`](Output) objects or to `WebMidi` itself * are also destroyed. * * @async * @returns {Promise} * * @throws {Error} The Web MIDI API is not supported by your environment. * * @since 2.0.0 */ async disable() { // This needs to be done right away to prevent racing conditions in listeners while the inputs // are being destroyed. if (this.interface) this.interface.onstatechange = undefined; return this._destroyInputsAndOutputs().then(() => { if (navigator && typeof navigator.close === "function") navigator.close(); // jzz this.interface = null; // also resets enabled, sysexEnabled /** * Event emitted once `WebMidi` has been successfully disabled. * * @event WebMidi#disabled * @type {object} * @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in * milliseconds since the navigation start of the document). * @property {WebMidi} target The object that triggered the event * @property {string} type `"disabled"` */ let event = { timestamp: this.time, target: this, type: "disabled" }; // Finally, trigger the 'disabled' event and then remove all listeners. this.emit("disabled", event); this.removeListener(); }); }; /** * Returns the [`Input`](Input) object that matches the specified ID string or `false` if no * matching input is found. As per the Web MIDI API specification, IDs are strings (not integers). * * Please note that IDs change from one host to another. For example, Chrome does not use the same * kind of IDs as Jazz-Plugin. * * @param id {string} The ID string of the input. IDs can be viewed by looking at the * [`WebMidi.inputs`](WebMidi#inputs) array. Even though they sometimes look like integers, IDs * are strings. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected input * * @returns {Input} An [`Input`](Input) object matching the specified ID string or `undefined` * if no matching input can be found. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getInputById(id, options = {disconnected: false}) { if (this.validation) { if (!this.enabled) throw new Error("WebMidi is not enabled."); if (!id) return; } if (options.disconnected) { for (let i = 0; i < this._disconnectedInputs.length; i++) { if ( this._disconnectedInputs[i]._midiInput && this._disconnectedInputs[i].id === id.toString() ) return this._disconnectedInputs[i]; } } else { for (let i = 0; i < this.inputs.length; i++) { if ( this.inputs[i]._midiInput && this.inputs[i].id === id.toString() ) return this.inputs[i]; } } }; /** * Returns the first [`Input`](Input) object whose name **contains** the specified string. Note * that the port names change from one environment to another. For example, Chrome does not report * input names in the same way as the Jazz-Plugin does. * * @param name {string} The non-empty string to look for within the name of MIDI inputs (such as * those visible in the [inputs](WebMidi#inputs) array). * * @returns {Input} The [`Input`](Input) that was found or `undefined` if no input contained the * specified name. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected input * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getInputByName(name, options = {disconnected: false}) { if (this.validation) { if (!this.enabled) throw new Error("WebMidi is not enabled."); if (!name) return; name = name.toString(); } if (options.disconnected) { for (let i = 0; i < this._disconnectedInputs.length; i++) { if (~this._disconnectedInputs[i].name.indexOf(name)) return this._disconnectedInputs[i]; } } else { for (let i = 0; i < this.inputs.length; i++) { if (~this.inputs[i].name.indexOf(name)) return this.inputs[i]; } } }; /** * Returns the first [`Output`](Output) object whose name **contains** the specified string. Note * that the port names change from one environment to another. For example, Chrome does not report * input names in the same way as the Jazz-Plugin does. * * @param name {string} The non-empty string to look for within the name of MIDI inputs (such as * those visible in the [`outputs`](#outputs) array). * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected output * * @returns {Output} The [`Output`](Output) that was found or `undefined` if no output matched * the specified name. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getOutputByName(name, options = {disconnected: false}) { if (this.validation) { if (!this.enabled) throw new Error("WebMidi is not enabled."); if (!name) return; name = name.toString(); } if (options.disconnected) { for (let i = 0; i < this._disconnectedOutputs.length; i++) { if (~this._disconnectedOutputs[i].name.indexOf(name)) return this._disconnectedOutputs[i]; } } else { for (let i = 0; i < this.outputs.length; i++) { if (~this.outputs[i].name.indexOf(name)) return this.outputs[i]; } } }; /** * Returns the [`Output`](Output) object that matches the specified ID string or `false` if no * matching output is found. As per the Web MIDI API specification, IDs are strings (not * integers). * * Please note that IDs change from one host to another. For example, Chrome does not use the same * kind of IDs as Jazz-Plugin. * * @param id {string} The ID string of the port. IDs can be viewed by looking at the * [`WebMidi.outputs`](WebMidi#outputs) array. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected output * * @returns {Output} An [`Output`](Output) object matching the specified ID string. If no * matching output can be found, the method returns `undefined`. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getOutputById(id, options = {disconnected: false}) { if (this.validation) { if (!this.enabled) throw new Error("WebMidi is not enabled."); if (!id) return; } if (options.disconnected) { for (let i = 0; i < this._disconnectedOutputs.length; i++) { if ( this._disconnectedOutputs[i]._midiOutput && this._disconnectedOutputs[i].id === id.toString() ) return this._disconnectedOutputs[i]; } } else { for (let i = 0; i < this.outputs.length; i++) { if ( this.outputs[i]._midiOutput && this.outputs[i].id === id.toString() ) return this.outputs[i]; } } }; /** * @private * @deprecated since version 3.0.0, use Utilities.toNoteNumber() instead. */ noteNameToNumber(name) { if (this.validation) { console.warn( "The noteNameToNumber() method is deprecated. Use " + "Utilities.toNoteNumber() instead." ); } return Utilities.toNoteNumber(name, this.octaveOffset); } /** * @private * @deprecated since 3.0.0, use Utilities.getNoteDetails() instead. */ getOctave(number) { if (this.validation) { console.warn("The getOctave()is deprecated. Use Utilities.getNoteDetails() instead"); number = parseInt(number); } if (!isNaN(number) && number >= 0 && number <= 127) { return Utilities.getNoteDetails(Utilities.offsetNumber(number, this.octaveOffset)).octave; } else { return false; } } /** * @private * @deprecated since 3.0.0, use Utilities.sanitizeChannels() instead. */ sanitizeChannels(channel) { if (this.validation) { console.warn("The sanitizeChannels() method has been moved to the utilities class."); } return Utilities.sanitizeChannels(channel); } /** * @private * @deprecated since version 3.0.0, use Utilities.sanitizeChannels() instead. */ toMIDIChannels(channel) { if (this.validation) { console.warn( "The toMIDIChannels() method has been deprecated. Use Utilities.sanitizeChannels() instead." ); } return Utilities.sanitizeChannels(channel); } /** * @private * @deprecated since version 3.0.0, use Utilities.guessNoteNumber() instead. */ guessNoteNumber(input) { if (this.validation) { console.warn( "The guessNoteNumber() method has been deprecated. Use Utilities.guessNoteNumber() instead." ); } return Utilities.guessNoteNumber(input, this.octaveOffset); } /** * @private * @deprecated since version 3.0.0, use Utilities.buildNoteArray() instead. */ getValidNoteArray(notes, options = {}) { if (this.validation) { console.warn( "The getValidNoteArray() method has been moved to the Utilities.buildNoteArray()" ); } return Utilities.buildNoteArray(notes, options); } /** * @private * @deprecated since version 3.0.0, use Utilities.toTimestamp() instead. */ convertToTimestamp(time) { if (this.validation) { console.warn( "The convertToTimestamp() method has been moved to Utilities.toTimestamp()." ); } return Utilities.toTimestamp(time); } /** * @return {Promise} * @private */ async _destroyInputsAndOutputs() { let promises = []; this.inputs.forEach(input => promises.push(input.destroy())); this.outputs.forEach(output => promises.push(output.destroy())); return Promise.all(promises).then(() => { this._inputs = []; this._outputs = []; }); } /** * @private */ _onInterfaceStateChange(e) { // If WebMidi is no longer enabled, silently ignore the event. Methods like getOutputById() // and getInputById() throw when WebMidi is disabled, and since this handler is triggered by a // browser event, client code cannot catch those errors — causing unrecoverable crashes (#546). if (!this.enabled) return; this._updateInputsAndOutputs(); /** * Event emitted when an [`Input`](Input) or [`Output`](Output) port is connected or * disconnected. This event is typically fired whenever a MIDI device is plugged in or * unplugged. Please note that it may fire several times if a device possesses multiple inputs * and/or outputs (which is often the case). * * @event WebMidi#portschanged * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred * (in milliseconds since the navigation start of the document). * @property {string} type `portschanged` * @property {WebMidi} target The object to which the listener was originally added (`WebMidi`) * @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that * triggered the event. * * @since 3.0.2 */ /** * Event emitted when an [`Input`](Input) or [`Output`](Output) becomes available. This event is * typically fired whenever a MIDI device is plugged in. Please note that it may fire several * times if a device possesses multiple inputs and/or outputs (which is often the case). * * @event WebMidi#connected * @type {object} * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred * (in milliseconds since the navigation start of the document). * @property {string} type `connected` * @property {WebMidi} target The object to which the listener was originally added (`WebMidi`) * @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that * triggered the event. */ /** * Event emitted when an [`Input`](Input) or [`Output`](Output) becomes unavailable. This event * is typically fired whenever a MIDI device is unplugged. Please note that it may fire several * times if a device possesses multiple inputs and/or outputs (which is often the case). * * @event WebMidi#disconnected * @type {object} * @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds * since the navigation start of the document). * @property {string} type `disconnected` * @property {WebMidi} target The object to which the listener was originally added (`WebMidi`) * @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that * triggered the event. */ let event = { timestamp: e.timeStamp, type: e.port.state, target: this }; // We check if "connection" is "open" because connected events are also triggered with // "connection=closed" if (e.port.state === "connected" && e.port.connection === "open") { if (e.port.type === "output") { event.port = this.getOutputById(e.port.id); } else if (e.port.type === "input") { event.port = this.getInputById(e.port.id); } // Emit "connected" event this.emit(e.port.state, event); // Make a shallow copy of the event so we can use it for the "portschanged" event const portsChangedEvent = Object.assign({}, event); portsChangedEvent.type = "portschanged"; this.emit(portsChangedEvent.type, portsChangedEvent); // We check if "connection" is "pending" because we do not always get the "closed" event } else if (e.port.state === "disconnected" && e.port.connection === "pending") { if (e.port.type === "input") { event.port = this.getInputById(e.port.id, {disconnected: true}); } else if (e.port.type === "output") { event.port = this.getOutputById(e.port.id, {disconnected: true}); } // Emit "disconnected" event this.emit(e.port.state, event); // Make a shallow copy of the event so we can use it for the "portschanged" event const portsChangedEvent = Object.assign({}, event); portsChangedEvent.type = "portschanged"; this.emit(portsChangedEvent.type, portsChangedEvent); } }; /** * @private */ async _updateInputsAndOutputs() { return Promise.all([ this._updateInputs(), this._updateOutputs() ]); }; /** * @private */ async _updateInputs() { // We must check for the existence of this.interface because it might have been closed via // WebMidi.disable(). if (!this.interface) return; // Check for items to remove from the existing array (because they are no longer being reported // by the MIDI back-end). for (let i = this._inputs.length - 1; i >= 0; i--) { const current = this._inputs[i]; const inputs = Array.from(this.interface.inputs.values()); if (! inputs.find(input => input === current._midiInput)) { // Instead of destroying removed inputs, we stash them in case they come back (which is the // case when the computer goes to sleep and is later brought back online). this._disconnectedInputs.push(current); this._inputs.splice(i, 1); } } // Array to hold pending promises from trying to open all input ports let promises = []; // Add new inputs (if not already present) this.interface.inputs.forEach(nInput => { // Check if the input is currently absent from the 'inputs' array. if (! this._inputs.find(input => input._midiInput === nInput) ) { // If the input has previously been stashed away, reuse it. If not, create a new one. let input = this._disconnectedInputs.find(input => input._midiInput === nInput); if (!input) input = new Input(nInput); this._inputs.push(input); promises.push(input.open()); } }); // Return a promise that resolves when all promises have resolved return Promise.all(promises); }; /** * @private */ async _updateOutputs() { // We must check for the existence of this.interface because it might have been closed via // WebMidi.disable(). if (!this.interface) return; // Check for items to remove from the existing array (because they are no longer being reported // by the MIDI back-end). for (let i = this._outputs.length - 1; i >= 0; i--) { const current = this._outputs[i]; const outputs = Array.from(this.interface.outputs.values()); if (! outputs.find(output => output === current._midiOutput)) { // Instead of destroying removed inputs, we stash them in case they come back (which is the // case when the computer goes to sleep and is later brought back online). this._disconnectedOutputs.push(current); this._outputs.splice(i, 1); } } // Array to hold pending promises from trying to open all output ports let promises = []; // Add new outputs (if not already present) this.interface.outputs.forEach(nOutput => { // Check if the output is currently absent from the 'outputs' array. if (! this._outputs.find(output => output._midiOutput === nOutput) ) { // If the output has previously been stashed away, reuse it. If not, create a new one. let output = this._disconnectedOutputs.find(output => output._midiOutput === nOutput); if (!output) output = new Output(nOutput); this._outputs.push(output); promises.push(output.open()); } }); // Return a promise that resolves when all sub-promises have resolved return Promise.all(promises); }; // injectPluginMarkup(parent) { // // // Silently ignore on Node.js // if (Utilities.isNode) return; // // // Default to if no parent is specified // if (!(parent instanceof Element) && !(parent instanceof HTMLDocument)) { // parent = document.body; // } // // // IE10 needs this: // // // // // Create markup and add to parent // const obj = document.createElement("object"); // obj.classid = "CLSID:1ACE1618-1C7D-4561-AEE1-34842AA85E90"; // IE // if (!obj.isJazz) obj.type = "audio/x-jazz"; // Standards-compliant // obj.style.visibility = "hidden"; // obj.style.width = obj.style.height = "0px"; // parent.appendChild(obj); // // } /** * Indicates whether access to the host's MIDI subsystem is active or not. * * @readonly * @type {boolean} */ get enabled() { return this.interface !== null; } /** * An array of all currently available MIDI inputs. * * @readonly * @type {Input[]} */ get inputs() { return this._inputs; } /** * @private * @deprecated */ get isNode() { if (this.validation) { console.warn("WebMidi.isNode has been deprecated. Use Utilities.isNode instead."); } return Utilities.isNode; } /** * @private * @deprecated */ get isBrowser() { if (this.validation) { console.warn("WebMidi.isBrowser has been deprecated. Use Utilities.isBrowser instead."); } return Utilities.isBrowser; } /** * An integer to offset the octave of notes received from external devices or sent to external * devices. * * When a MIDI message comes in on an input channel the reported note name will be offset. For * example, if the `octaveOffset` is set to `-1` and a [`"noteon"`](InputChannel#event:noteon) * message with MIDI number 60 comes in, the note will be reported as C3 (instead of C4). * * By the same token, when [`OutputChannel.playNote()`](OutputChannel#playNote) is called, the * MIDI note number being sent will be offset. If `octaveOffset` is set to `-1`, the MIDI note * number sent will be 72 (instead of 60). * * @type {number} * * @since 2.1 */ get octaveOffset() { return this._octaveOffset; } set octaveOffset(value) { if (this.validation) { value = parseInt(value); if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer."); } this._octaveOffset = value; } /** * An array of all currently available MIDI outputs as [`Output`](Output) objects. * * @readonly * @type {Output[]} */ get outputs() { return this._outputs; } /** * Indicates whether the environment provides support for the Web MIDI API or not. * * **Note**: in environments that do not offer built-in MIDI support, this will report `true` if * the * [`navigator.requestMIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) * function is available. For example, if you have installed WebMIDIAPIShim.js but no plugin, this * property will be `true` even though actual support might not be there. * * @readonly * @type {boolean} */ get supported() { return (typeof navigator !== "undefined" && !!navigator.requestMIDIAccess); } /** * Indicates whether MIDI system exclusive messages have been activated when WebMidi.js was * enabled via the [`enable()`](#enable) method. * * @readonly * @type boolean */ get sysexEnabled() { return !!(this.interface && this.interface.sysexEnabled); } /** * The elapsed time, in milliseconds, since the time * [origin](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin). * Said simply, it is the number of milliseconds that passed since the page was loaded. Being a * floating-point number, it has sub-millisecond accuracy. According to the * [documentation](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp), the * time should be accurate to 5 µs (microseconds). However, due to various constraints, the * browser might only be accurate to one millisecond. * * Note: `WebMidi.time` is simply an alias to `performance.now()`. * * @type {DOMHighResTimeStamp} * @readonly */ get time() { return performance.now(); } /** * The version of the library as a [semver](https://semver.org/) string. * * @readonly * @type string */ get version() { return "[VI]{version}[/VI]"; } /** * The flavour of the library. Can be one of: * * * `esm`: ECMAScript Module * * `cjs`: CommonJS Module * * `iife`: Immediately-Invoked Function Expression * * @readonly * @type string * @since 3.0.25 */ get flavour() { return "__flavour__"; // will be replaced during bundling by the correct identifier } /** * @private * @deprecated since 3.0.0. Use Enumerations.CHANNEL_EVENTS instead. */ get CHANNEL_EVENTS() { if (this.validation) { console.warn( "The CHANNEL_EVENTS enum has been moved to Enumerations.CHANNEL_EVENTS." ); } return Enumerations.CHANNEL_EVENTS; } /** * @private * @deprecated since 3.0.0. Use Enumerations.SYSTEM_MESSAGES instead. */ get MIDI_SYSTEM_MESSAGES() { if (this.validation) { console.warn( "The MIDI_SYSTEM_MESSAGES enum has been moved to " + "Enumerations.SYSTEM_MESSAGES." ); } return Enumerations.SYSTEM_MESSAGES; } /** * @private * @deprecated since 3.0.0. Use Enumerations.CHANNEL_MODE_MESSAGES instead */ get MIDI_CHANNEL_MODE_MESSAGES() { if (this.validation) { console.warn( "The MIDI_CHANNEL_MODE_MESSAGES enum has been moved to " + "Enumerations.CHANNEL_MODE_MESSAGES." ); } return Enumerations.CHANNEL_MODE_MESSAGES; } /** * @private * @deprecated since 3.0.0. Use Enumerations.CONTROL_CHANGE_MESSAGES instead. */ get MIDI_CONTROL_CHANGE_MESSAGES() { if (this.validation) { console.warn( "The MIDI_CONTROL_CHANGE_MESSAGES enum has been replaced by the " + "Enumerations.CONTROL_CHANGE_MESSAGES array." ); } return Enumerations.MIDI_CONTROL_CHANGE_MESSAGES; } /** * @deprecated since 3.0.0. Use Enumerations.REGISTERED_PARAMETERS instead. * @private */ get MIDI_REGISTERED_PARAMETER() { if (this.validation) { console.warn( "The MIDI_REGISTERED_PARAMETER enum has been moved to " + "Enumerations.REGISTERED_PARAMETERS." ); } return Enumerations.REGISTERED_PARAMETERS; } /** * @deprecated since 3.0.0. * @private */ get NOTES() { if (this.validation) { console.warn("The NOTES enum has been deprecated."); } return ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; } } // Export singleton instance of WebMidi class. The 'constructor' is nulled so that it cannot be used // to instantiate a new WebMidi object or extend it. However, it is not freezed so it remains // extensible (properties can be added at will). const wm = new WebMidi(); wm.constructor = null; export {Enumerations} from "./Enumerations.js"; export {Forwarder} from "./Forwarder.js"; export {Input} from "./Input.js"; export {InputChannel} from "./InputChannel.js"; export {Message} from "./Message.js"; export {Note} from "./Note.js"; export {Output} from "./Output.js"; export {OutputChannel} from "./OutputChannel.js"; export {Utilities} from "./Utilities.js"; export {wm as WebMidi}; ================================================ FILE: test/Enumerations.test.js ================================================ const expect = require("chai").expect; const {Enumerations} = require("../dist/cjs/webmidi.cjs.js"); // VERIFIED describe("Enumerations Object", function() { describe("get CHANNEL_MESSAGES()", function() { it("should return an object with valid properties", function() { expect(Enumerations.CHANNEL_MESSAGES.pitchbend).to.equal(0xE); }); }); describe("get MIDI_REGISTERED_PARAMETER()", function() { it("should return an object with valid properties", function() { expect(Enumerations.REGISTERED_PARAMETERS.rollangle[0]).to.equal(0x3D); expect(Enumerations.REGISTERED_PARAMETERS.rollangle[1]).to.equal(0x08); }); }); describe("get CONTROL_CHANGE_MESSAGES()", function() { it("should return an an array with valid properties", function() { expect(Enumerations.CONTROL_CHANGE_MESSAGES[73].name).to.equal("attacktime"); }); }); // Legacy (remove in v4) describe("get MIDI_CONTROL_CHANGE_MESSAGES()", function() { it("should return an object with valid properties", function() { expect(Enumerations.MIDI_CONTROL_CHANGE_MESSAGES.registeredparameterfine).to.equal(101); }); }); }); ================================================ FILE: test/Forwarder.test.js ================================================ const expect = require("chai").expect; const {Message, WebMidi, Note, Forwarder} = require("../dist/cjs/webmidi.cjs.js"); const midi = require("@julusian/midi"); // The virtual port is an "external" device so an input is seen as an output by WebMidi. To avoid // confusion, the naming scheme adopts WebMidi's perspective. let VIRTUAL_OUTPUT = new midi.Input(); let VIRTUAL_OUTPUT_NAME = "Virtual Output"; let WEBMIDI_OUTPUT; describe("Forwarder Object", function() { before(function () { VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); VIRTUAL_OUTPUT.ignoreTypes(false, false, false); // enable sysex, timing & active sensing }); after(function () { VIRTUAL_OUTPUT.closePort(); }); beforeEach("Check support and enable WebMidi.js", async function () { await WebMidi.enable({sysex: true}); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); }); afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); }); describe("constructor()", function() { it("should throw when an invalid destination is specified", function() { // Arrange const items = [ "abc", 123, new Note(64), ]; // Act // Assert items.forEach(item => { expect(() => new Forwarder(item)).to.throw(TypeError); }); }); it("should throw when an invalid type is specified", function() { // Arrange const types = [ "abc", 123, ]; // Act // Assert types.forEach(type => { expect(() => { new Forwarder(WEBMIDI_OUTPUT, {types: type}); }).to.throw(TypeError); }); }); it("should throw when an invalid channel is specified", function() { // Arrange const items = [ 0, -1, 17, ]; // Act // Assert items.forEach(item => { expect(() => { new Forwarder(WEBMIDI_OUTPUT, {channels: item}); }).to.throw(TypeError); }); }); }); describe("forward()", function() { it("should forward the message to the forwarder's destination(s)", function(done) { // Arrange const data = [0x90, 64, 127]; const msg = new Message(data); const forwarder = new Forwarder(WEBMIDI_OUTPUT); VIRTUAL_OUTPUT.on("message", assert); // Act forwarder.forward(msg); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(msg.data); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should not forward messages when 'suspended' is true", function(done) { // Arrange const data = [0x90, 64, 127]; const msg = new Message(data); const forwarder = new Forwarder(WEBMIDI_OUTPUT); forwarder.suspended = true; VIRTUAL_OUTPUT.on("message", assert); // Act forwarder.forward(msg); const id = setTimeout(() => { VIRTUAL_OUTPUT.removeAllListeners(); done(); }, 250); // Assert function assert(deltaTime, message) { VIRTUAL_OUTPUT.removeAllListeners(); clearTimeout(id); done(new Error("Callback should not have been triggered. " + message)); } }); it("should not forward message if it does not match channel", function(done) { // Arrange const msg1 = new Message([0x90, 64, 127]); // note on on ch. 1 const msg2 = new Message([0x94, 64, 127]); // note on on ch. 5 const msg3 = new Message([0x9A, 64, 127]); // note on on ch. 11 const msg4 = new Message([0x9F, 64, 127]); // note on on ch. 16 const channel = 10; const forwarder = new Forwarder(WEBMIDI_OUTPUT, {channels: channel}); VIRTUAL_OUTPUT.on("message", assert); // Act const id = setTimeout(() => { VIRTUAL_OUTPUT.removeAllListeners(); done(); }, 250); forwarder.forward(msg1); forwarder.forward(msg2); forwarder.forward(msg3); forwarder.forward(msg4); // Assert function assert(deltaTime, message) { VIRTUAL_OUTPUT.removeAllListeners(); clearTimeout(id); done(new Error("Callback should not have been triggered. " + message)); } }); it("should not forward message if it does not match type", function(done) { // Arrange const target = {name: "pitchbend", number: 0xE0}; // pitchbend const messages = [ 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0 ]; const forwarder = new Forwarder(WEBMIDI_OUTPUT, {types: target.name}); VIRTUAL_OUTPUT.on("message", assert); // Act const id = setTimeout(() => { VIRTUAL_OUTPUT.removeAllListeners(); done(); }, 250); messages.forEach(i => { forwarder.forward(new Message([i, 64, 127])); }); // Assert function assert(deltaTime, message) { VIRTUAL_OUTPUT.removeAllListeners(); clearTimeout(id); done(new Error("Callback should not have been triggered. " + message)); } }); }); }); ================================================ FILE: test/Input.test.js ================================================ const expect = require("chai").expect; const midi = require("@julusian/midi"); const sinon = require("sinon"); const {WebMidi, Enumerations, Forwarder} = require("../dist/cjs/webmidi.cjs.js"); // The virtual port is an "external" device so an output is seen as an input by WebMidi. To avoid // confusion, the naming scheme adopts WebMidi's perspective. let VIRTUAL_INPUT = new midi.Output(); let VIRTUAL_INPUT_NAME = "Virtual Input"; let WEBMIDI_INPUT; // The virtual port is an "external" device so an input is seen as an output by WebMidi. To avoid // confusion, the naming scheme adopts WebMidi's perspective. let VIRTUAL_OUTPUT = new midi.Input(); let VIRTUAL_OUTPUT_NAME = "Virtual Output"; let WEBMIDI_OUTPUT; describe("Input Object", function() { before(function () { VIRTUAL_INPUT.openVirtualPort(VIRTUAL_INPUT_NAME); VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); VIRTUAL_OUTPUT.ignoreTypes(false, false, false); // enable sysex, timing & active sensing }); after(function () { VIRTUAL_INPUT.closePort(); VIRTUAL_OUTPUT.closePort(); }); beforeEach("Check support and enable WebMidi.js", async function () { await WebMidi.enable({sysex: true}); WEBMIDI_INPUT = WebMidi.getInputByName(VIRTUAL_INPUT_NAME); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); }); afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); }); it("should dispatch events when receiving sysex messages (normal)", function (done) { // Arrange let data = [ Enumerations.MIDI_SYSTEM_MESSAGES.sysex, 0x42, // Korg 1, // data 2, // data 3, // data Enumerations.MIDI_SYSTEM_MESSAGES.sysexend ]; WEBMIDI_INPUT.addListener("sysex", assert); // Act VIRTUAL_INPUT.sendMessage(data); // Assert function assert(e) { expect(e.message.data).to.have.ordered.members(data); done(); } }); it("should dispatch events when receiving system common MIDI messages (normal)", function (done) { // Arrange let items = [ {type: "timecode", data: [0xF1, 0x36]}, {type: "songposition", data: [12, 34]}, {type: "songselect", data: [123]}, {type: "tunerequest", data: []} ]; let index = 0; items.forEach(item => { WEBMIDI_INPUT.addListener(item.type, assert); }); // Act items.forEach(item => { VIRTUAL_INPUT.sendMessage( [Enumerations.MIDI_SYSTEM_MESSAGES[item.type]].concat(item.data) ); }); // Assert function assert(e) { expect(e.type).to.equal(items[index].type); index++; if (index >= items.length) done(); } }); it("should dispatch events when receiving system common MIDI messages (legacy)", function (done) { // Arrange let items = [ {type: "timecode", data: [0xF1, 0x36]}, {type: "songposition", data: [12, 34]}, {type: "songselect", data: [123]}, {type: "tunerequest", data: []} ]; let index = 0; items.forEach(item => { WEBMIDI_INPUT.addListener(item.type, undefined, assert); }); // Act items.forEach(item => { VIRTUAL_INPUT.sendMessage( [Enumerations.MIDI_SYSTEM_MESSAGES[item.type]].concat(item.data) ); }); // Assert function assert(e) { expect(e.type).to.equal(items[index].type); index++; if (index >= items.length) done(); } }); it("should dispatch events when receiving realtime MIDI messages (normal)", function (done) { // Arrange let events = [ "clock", "start", "continue", "stop", "activesensing", "reset" ]; let index = 0; events.forEach(event => { WEBMIDI_INPUT.addListener(event, assert); }); // Act events.forEach(event => { VIRTUAL_INPUT.sendMessage( [Enumerations.MIDI_SYSTEM_MESSAGES[event]] ); }); // Assert function assert(e) { let event = events[index]; expect(e.data).to.have.ordered.members([Enumerations.MIDI_SYSTEM_MESSAGES[event]]); index++; if (index >= events.length) done(); } }); it("should dispatch events when receiving realtime MIDI messages (legacy)", function (done) { // Arrange let events = [ "clock", "start", "continue", "stop", "activesensing", "reset" ]; let index = 0; events.forEach(event => { WEBMIDI_INPUT.addListener(event, undefined, assert); }); // Act events.forEach(event => { VIRTUAL_INPUT.sendMessage( [Enumerations.MIDI_SYSTEM_MESSAGES[event]] ); }); // Assert function assert(e) { let event = events[index]; expect(e.data).to.have.ordered.members([Enumerations.MIDI_SYSTEM_MESSAGES[event]]); index++; if (index >= events.length) done(); } }); it("should dispatch midimessage event when receiving any messages (normal)", function (done) { // Arrange let messages = [ [0x80, 48, 87], // Note off [0x90, 52, 64], // Note on [0xA0, 60, 83], // Key pressure [0xB0, 67, 92], // Control change [0xC0, 88], // Program change [0xD0, 93], // Program change [0xE0, 95, 101], // Pitch bend [250] // Start ]; let index = 0; WEBMIDI_INPUT.addListener("midimessage", assert); // Act messages.forEach(msg => { VIRTUAL_INPUT.sendMessage(msg); }); // Assert function assert(e) { expect(e.data).to.have.ordered.members(messages[index]); index++; if (index >= messages.length) done(); } }); it("should dispatch midimessage event when receiving any messages (legacy)", function (done) { // Arrange let messages = [ [0x80, 48, 87], // Note off [0x90, 52, 64], // Note on [0xA0, 60, 83], // Key pressure [0xB0, 67, 92], // Control change [0xC0, 88], // Program change [0xD0, 93], // Program change [0xE0, 95, 101], // Pitch bend [250] // Start ]; let index = 0; WEBMIDI_INPUT.addListener("midimessage", undefined, assert); // Act messages.forEach(msg => { VIRTUAL_INPUT.sendMessage(msg); }); // Assert function assert(e) { expect(e.data).to.have.ordered.members(messages[index]); index++; if (index >= messages.length) done(); } }); it("should trigger 'opened' event", function (done) { // Arrange const input = WebMidi.getInputByName(VIRTUAL_INPUT_NAME); input.addListener("opened", () => done()); input.addListener("closed", () => input.open()); // Act input.close(); }); it("should trigger 'closed' event", function (done) { // Arrange const input = WebMidi.getInputByName(VIRTUAL_INPUT_NAME); input.addListener("closed", () => done()); // Act input.close(); }); it("should trigger 'disconnected' event"); // below does not work... // it.only("should trigger 'disconnected' event", function (done) { // // // Arrange // const input = WebMidi.getInputByName(VIRTUAL_INPUT_NAME); // input.addListener("disconnected", () => done()); // // // Act // VIRTUAL_INPUT.closePort(); // // setTimeout(() => { // VIRTUAL_INPUT.openVirtualPort(VIRTUAL_INPUT_NAME); // }, 100); // // }); describe("addListener()", function() { it("should add listeners to all specified channels (normal)", function() { // Arrange let l1 = () => {}; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let event = "noteon"; // Act WEBMIDI_INPUT.addListener(event, l1, {channels: channels}); // Assert WEBMIDI_INPUT.channels.forEach(ch => { expect(ch.hasListener(event, l1)).to.be.true; }); expect( WEBMIDI_INPUT.hasListener(event, l1, {channels: channels}) ).to.be.true; }); it("should add listeners to all specified channels (legacy)", function() { // Arrange let l1 = () => {}; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let event = "noteon"; // Act WEBMIDI_INPUT.addListener(event, channels, l1); // Assert WEBMIDI_INPUT.channels.forEach(ch => { expect(ch.hasListener(event, l1)).to.be.true; }); expect( WEBMIDI_INPUT.hasListener(event, channels, l1) ).to.be.true; }); it("should add listeners for input-wide messages (normal)", function() { // Arrange let l1 = () => {}; // Act Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(key => { WEBMIDI_INPUT.addListener(key, l1); }); // Assert Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(key => { expect(WEBMIDI_INPUT.hasListener(key, l1)).to.be.true; }); }); it("should add listeners for input-wide messages (legacy)", function() { // Arrange let l1 = () => {}; // Act Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(key => { WEBMIDI_INPUT.addListener(key, undefined, l1); }); // Assert Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(key => { expect(WEBMIDI_INPUT.hasListener(key, undefined, l1)).to.be.true; }); }); it("should throw error for invalid event types (normal)", function() { // Arrange let values = [undefined, null, "", NaN, [], {}, 0]; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // Act values.forEach(assert); // Assert function assert(event) { expect(() => { WEBMIDI_INPUT.addListener(event, () => {}, {channels: channels}); }).to.throw(TypeError); } }); it("should throw error for invalid event types (legacy)", function() { // Arrange let values = [undefined, null, "", NaN, [], {}, 0]; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // Act values.forEach(assert); // Assert function assert(event) { expect(() => { WEBMIDI_INPUT.addListener(event, channels, () => {}); }).to.throw(TypeError); } }); it("should add listener to all channels when none is specified (normal)", function () { // Arrange // Act const listener = WEBMIDI_INPUT.addListener("noteon", () => {}); // Assert expect(listener.length).to.equal(16); }); it("should add listener to all channels when none is specified (legacy)", function () { // Arrange // Act const listener = WEBMIDI_INPUT.addListener("noteon", undefined, () => {}); // Assert expect(listener.length).to.equal(16); }); it("should return a single 'Listener' for system messages (normal)", function() { // Arrange let callbacks = []; let listeners = []; // Act Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(function(key, index) { callbacks[index] = () => {}; listeners[index] = WEBMIDI_INPUT.addListener(key, callbacks[index]); }); // Assert Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(function(key, index) { expect(Array.isArray(listeners[index])).to.be.false; expect(listeners[index].callback === callbacks[index]).to.be.true; }); }); it("should return a single 'Listener' for system messages (normal)", function() { // Arrange let callbacks = []; let listeners = []; // Act Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(function(key, index) { callbacks[index] = () => {}; listeners[index] = WEBMIDI_INPUT.addListener(key, undefined, callbacks[index]); }); // Assert Object.keys(Enumerations.MIDI_SYSTEM_MESSAGES).forEach(function(key, index) { expect(Array.isArray(listeners[index])).to.be.false; expect(listeners[index].callback === callbacks[index]).to.be.true; }); }); it("should return an array of 'Listener' objects for channel messages (normal)", function() { // Arrange let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let callbacks = []; let listeners = []; Object.keys(Enumerations.CHANNEL_MESSAGES).forEach((key, index) => { callbacks[index] = () => {}; listeners[index] = WEBMIDI_INPUT.addListener(key, callbacks[index], {channels: channels}); }); Object.keys(Enumerations.CHANNEL_MESSAGES).forEach((key, index) => { expect(listeners[index].length).to.equal(channels.length); expect(listeners[index][0].callback === callbacks[index]).to.be.true; }); }); it("should return an array of 'Listener' objects for channel messages (legacy)", function() { // Arrange let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let callbacks = []; let listeners = []; Object.keys(Enumerations.CHANNEL_MESSAGES).forEach((key, index) => { callbacks[index] = () => {}; listeners[index] = WEBMIDI_INPUT.addListener(key, channels, callbacks[index]); }); Object.keys(Enumerations.CHANNEL_MESSAGES).forEach((key, index) => { expect(listeners[index].length).to.equal(channels.length); expect(listeners[index][0].callback === callbacks[index]).to.be.true; }); }); it("should throw if channels are specified but listener not a function (normal)", function() { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; expect(() => { WEBMIDI_INPUT.addListener(event, undefined, {channels: channels}); }).to.throw(TypeError); }); it("should throw if channels are specified but listener not a function (legacy)", function() { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; expect(() => { WEBMIDI_INPUT.addListener(event, channels); }).to.throw(Error); }); it("should ignore invalid channels (normal)", function() { // Arrange let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, "", NaN, [], {}, 0]; let channels = valid.concat(invalid); // Act let listeners = WEBMIDI_INPUT.addListener("noteon", () => {}, {channels: channels}); // Assert expect(listeners.length).to.equal(valid.length); }); it("should ignore invalid channels (legacy)", function() { // Arrange let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, "", NaN, [], {}, 0]; let channels = valid.concat(invalid); // Act let listeners = WEBMIDI_INPUT.addListener("noteon", channels, () => {}); // Assert expect(listeners.length).to.equal(valid.length); }); }); describe("addOneTimeListener()", function() { it("should properly call 'addListener()' with appropriate parameters", function () { // Arrange let spy = sinon.spy(WEBMIDI_INPUT.channels[1], "addListener"); let event = "noteon"; let listener = () => {}; let options = { channels: 1, context: {}, prepend: true, duration: 1000, arguments: [1, 2, 3] }; // Act WEBMIDI_INPUT.addOneTimeListener(event, listener, options); // Assert let args = spy.args[0]; expect(args[0]).to.equal(event); expect(args[1]).to.equal(listener); expect(args[2].context).to.equal(options.context); expect(args[2].prepend).to.equal(options.prepend); expect(args[2].duration).to.equal(options.duration); expect(args[2].arguments).to.equal(options.arguments); expect(args[2].remaining).to.equal(1); }); it("should call 'addListener()' for all specified channels", function () { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let listener = () => {}; let spies = []; // Act channels.forEach(ch => { spies.push(sinon.spy(WEBMIDI_INPUT.channels[ch], "addListener")); }); WEBMIDI_INPUT.addOneTimeListener(event, listener, {channels: channels}); // Assert spies.forEach(spy => { expect(spy.args[0][2].remaining).to.equal(1); expect(spy.calledOnce).to.be.true; }); }); }); describe("constructor()", function() { it("should set up all 'InputChannel' objects", function() { expect(WEBMIDI_INPUT.channels.length).to.equal(16+1); for (let i = 1; i <= 16; i++) { expect(WEBMIDI_INPUT.channels[i].number).to.equal(i); } }); }); describe("close()", function() { it("should close the port", async function () { // Act await WEBMIDI_INPUT.close(); // Assert expect(WEBMIDI_INPUT.connection).to.equal("closed"); }); }); describe("destroy()", function() { it("should destroy the 'Input'", async function() { // Act await WEBMIDI_INPUT.destroy(); // Assert try { WEBMIDI_INPUT.name; } catch (e) { await Promise.resolve(); } if (WEBMIDI_INPUT.channels.length !== 0) return Promise.reject(); if (WEBMIDI_INPUT.hasListener() === true) return Promise.reject(); }); }); describe("hasListener()", function() { it("should call channel's 'hasListener()' for all specified channels (normal)", function () { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let listener = () => {}; let spies = []; // Act channels.forEach(ch => { spies.push(sinon.spy(WEBMIDI_INPUT.channels[ch], "hasListener")); }); WEBMIDI_INPUT.addListener(event, listener, {channels: channels}); WEBMIDI_INPUT.hasListener(event, listener, {channels: channels}); // Assert spies.forEach(spy => { expect(spy.calledOnce).to.be.true; }); }); it("should call channel's 'hasListener()' for all specified channels (legacy)", function () { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let listener = () => {}; let spies = []; // Act channels.forEach(ch => { spies.push(sinon.spy(WEBMIDI_INPUT.channels[ch], "hasListener")); }); WEBMIDI_INPUT.addListener(event, channels, listener); WEBMIDI_INPUT.hasListener(event, channels, listener); // Assert spies.forEach(spy => { expect(spy.calledOnce).to.be.true; }); }); it("should call input's 'hasListener()' for all input-wide events", function () { // Arrange let event = "clock"; let listener = () => {}; let spy = sinon.spy(WEBMIDI_INPUT, "hasListener"); // Act WEBMIDI_INPUT.addListener(event, listener); WEBMIDI_INPUT.hasListener(event, listener); // Assert expect(spy.calledOnce).to.be.true; expect(spy.args[0][0]).to.equal(event); expect(spy.args[0][1]).to.equal(listener); }); }); describe("open()", async function() { it("should open the port", async function () { // Act await WEBMIDI_INPUT.close(); await WEBMIDI_INPUT.open(); // Assert expect(WEBMIDI_INPUT.connection).to.equal("open"); }); }); describe("removeListener()", function() { it("should call channel's 'removeListener()' for all specified channels (normal)", function () { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let listener = () => {}; let spies = []; // Act channels.forEach(ch => { spies.push(sinon.spy(WEBMIDI_INPUT.channels[ch], "removeListener")); }); WEBMIDI_INPUT.removeListener(event, listener, {channels: channels}); // Assert spies.forEach(spy => { expect(spy.calledOnce).to.be.true; }); }); it("should call channel's 'removeListener()' for all specified channels (legacy)", function () { // Arrange let event = "noteon"; let channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let listener = () => {}; let spies = []; // Act channels.forEach(ch => { spies.push(sinon.spy(WEBMIDI_INPUT.channels[ch], "removeListener")); }); WEBMIDI_INPUT.removeListener(event, channels, listener); // Assert spies.forEach(spy => { expect(spy.calledOnce).to.be.true; }); }); it("should call input's 'removeListener()' for all input-wide events (normal)", function () { // Arrange let event = "clock"; let listener = () => {}; let spy = sinon.spy(WEBMIDI_INPUT, "removeListener"); // Act WEBMIDI_INPUT.removeListener(event, listener); // Assert expect(spy.calledOnce).to.be.true; expect(spy.args[0][0]).to.equal(event); expect(spy.args[0][1]).to.equal(listener); }); it("should call input's 'removeListener()' for all input-wide events (legacy)", function () { // Arrange let event = "clock"; let listener = () => {}; let spy = sinon.spy(WEBMIDI_INPUT, "removeListener"); // Act WEBMIDI_INPUT.removeListener(event, undefined, listener); // Assert expect(spy.calledOnce).to.be.true; expect(spy.args[0][0]).to.equal(event); expect(spy.args[0][2]).to.equal(listener); }); }); describe("addForwarder()", function() { it("should add a forwarder and return it", function() { // Arrange // Act const forwarder = WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT); // Assert expect(WEBMIDI_INPUT.hasForwarder(forwarder)).to.be.true; }); it("should forward message to specified output", function(done) { // Arrange const data = [0x90, 64, 127]; WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT); VIRTUAL_OUTPUT.on("message", assert); // Act VIRTUAL_INPUT.sendMessage(data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(data); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should forward message to specified output (with channel filter)", function(done) { // Arrange let target = [0x99, 64, 127]; // noteon on channel 10 const data = [ [0x90, 64, 127], // noteon on channel 1 [0x81, 64, 127], // noteoff on channel 2 target, ]; WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT, {channels: 10}); VIRTUAL_OUTPUT.on("message", assert); // Act data.forEach(item => VIRTUAL_INPUT.sendMessage(item)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(target); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should not forward message to filtered outputs (with channel filter)", function(done) { // Arrange const channel = 10; WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT, {channels: channel}); VIRTUAL_OUTPUT.on("message", assert); // Act [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] .filter(x => x !== channel).forEach(i => { VIRTUAL_INPUT.sendMessage([0x90 + i - 1, 64, 127]); }); // Assert const id = setTimeout(() => { VIRTUAL_OUTPUT.removeAllListeners(); done(); }, 750); function assert(deltaTime, message) { VIRTUAL_OUTPUT.removeAllListeners(); clearTimeout(id); done(new Error("Callback should not have been triggered. " + message)); } }); it("should forward message to specified output (with type filter)", function(done) { // Arrange let type = "controlchange"; const target = [0xB2, 64, 127]; const data = [ [0x90, 64, 127], // noteon on channel 1 [0x81, 64, 127], // noteoff on channel 2 target // control change on channel 3 ]; WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT, {types: type}); VIRTUAL_OUTPUT.on("message", assert); // Act data.forEach(item => VIRTUAL_INPUT.sendMessage(item)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(target); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should not forward message to filtered outputs (with type filter)", function(done) { // Arrange const target = {name: "pitchbend", number: 0xE0}; // pitchbend const messages = [ 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0 ]; WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT, {types: target.name}); VIRTUAL_OUTPUT.on("message", assert); // Act messages.filter(x => x !== target.number).forEach(i => { VIRTUAL_INPUT.sendMessage([i, 64, 127]); }); // Assert const id = setTimeout(() => { VIRTUAL_OUTPUT.removeAllListeners(); done(); }, 750); function assert(deltaTime, message) { VIRTUAL_OUTPUT.removeAllListeners(); clearTimeout(id); done(new Error("Callback should not have been triggered. " + message)); } }); }); describe("removeForwarder()", function() { it("should remove specified forwarder", function() { // Arrange const forwarder = WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT); // Act WEBMIDI_INPUT.removeForwarder(forwarder); // Assert expect(WEBMIDI_INPUT.hasForwarder(forwarder)).to.be.false; }); }); describe("hasForwarder()", function() { it("should correctly report prsence of forwarder", function() { // Arrange const forwarder1 = WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT); const forwarder2 = WEBMIDI_INPUT.addForwarder(WEBMIDI_OUTPUT); // Act WEBMIDI_INPUT.removeForwarder(forwarder1); // Assert expect(WEBMIDI_INPUT.hasForwarder(forwarder1)).to.be.false; expect(WEBMIDI_INPUT.hasForwarder(forwarder2)).to.be.true; expect(WEBMIDI_INPUT.hasForwarder(new Forwarder(WEBMIDI_OUTPUT))).to.be.false; }); }); }); ================================================ FILE: test/InputChannel.test.js ================================================ const expect = require("chai").expect; const midi = require("@julusian/midi"); const {WebMidi, Utilities, Enumerations, Note} = require("../dist/cjs/webmidi.cjs.js"); // Create virtual MIDI input port. Being an external device, the virtual device's output is seen as // an input from WebMidi's perspective. To avoid confusion, the property names adopt WebMidi's point // of view. let VIRTUAL_INPUT = { PORT: new midi.Output(), NAME: "Virtual input" }; let WEBMIDI_INPUT; describe("InputChannel Object", function() { before(function () { VIRTUAL_INPUT.PORT.openVirtualPort(VIRTUAL_INPUT.NAME); }); after(function () { VIRTUAL_INPUT.PORT.closePort(); }); beforeEach("Check support and enable", async function () { await WebMidi.enable(); WEBMIDI_INPUT = WebMidi.getInputByName(VIRTUAL_INPUT.NAME); }); afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); WEBMIDI_INPUT = undefined; }); it("should dispatch 'midimessage' events for all channel voice MIDI messages", function (done) { // Arrange let event = "midimessage"; let messages = [ [0x80, 48, 87], // Note off [0x90, 52, 64], // Note on [0xA0, 60, 83], // Key pressure [0xB0, 67, 92], // Control change [0xC0, 88], // Program change [0xD0, 93], // Channel aftertouch [0xE0, 95, 101], // Pitch bend [0xB0, 120, 0], // All sound off [0xB0, 121, 0], // reset all controllers [0xB0, 122, 0], // Local control off [0xB0, 122, 127], // Local control on [0xB0, 123, 0], // All notes off [0xB0, 126, 0], // Mono mode on (poly mode off) [0xB0, 127, 0] // Mono mode off (poly mode on) ]; let channel = WEBMIDI_INPUT.channels[1]; let index = 0; channel.addListener(event, assert); // Act messages.forEach(message => { VIRTUAL_INPUT.PORT.sendMessage(message); }); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.data).to.have.ordered.members(messages[index]); index++; if (index >= messages.length) done(); } }); it("should dispatch event for inbound 'noteoff' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "noteoff"; let velocity = 87; let status = 0x80; let index = 0; channel.addListener(event, assert); // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, velocity]); } // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.note.identifier).to.equal(Utilities.toNoteIdentifier(index)); expect(e.note.attack).to.equal(0); // the note must have an attack of 0 to be a noteoff expect(e.note.rawRelease).to.equal(velocity); expect(e.note.duration).to.equal(WebMidi.defaults.note.duration); expect(e.value).to.equal(Utilities.from7bitToFloat(velocity)); expect(e.rawValue).to.equal(velocity); expect(e.target).to.equal(channel); index++; if (index > 127) done(); } }); it("should dispatch 'noteoff' event when receiving 'noteon' with 0 velocity", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "noteoff"; let velocity = 0; let status = 0x90; // note on let index = 0; channel.addListener(event, assert); // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, velocity]); } // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.note.identifier).to.equal(Utilities.toNoteIdentifier(index)); expect(e.note.attack).to.equal(0); // the note must have an attack of 0 to be a noteoff expect(e.note.rawRelease).to.equal(velocity); expect(e.note.duration).to.equal(WebMidi.defaults.note.duration); expect(e.value).to.equal(Utilities.from7bitToFloat(velocity)); expect(e.rawValue).to.equal(velocity); expect(e.target).to.equal(channel); index++; if (index > 127) done(); } }); it("should report correct octave for 'noteoff' event (with octaveOffset)", function (done) { // Arrange const channel = WEBMIDI_INPUT.channels[1]; const event = "noteoff"; const message = [0x80, 60, 64]; WebMidi.octaveOffset = -1; WEBMIDI_INPUT.octaveOffset = -1; channel.octaveOffset = -1; const offset = WebMidi.octaveOffset + WEBMIDI_INPUT.octaveOffset + channel.octaveOffset; const result = 4 + offset; // Act channel.addListener(event, assert); VIRTUAL_INPUT.PORT.sendMessage(message); // Assert function assert(e) { expect(e.note.octave).to.equal(result); WebMidi.octaveOffset = 0; done(); } }); it("should dispatch event for inbound 'noteon' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "noteon"; let velocity = 64; let status = 0x90; let index = 0; channel.addListener(event, assert); // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, velocity]); } // Assert function assert(e) { expect(e.type).to.equal(event); expect(Utilities.toNoteNumber(e.note.identifier)).to.equal(index); expect(e.rawValue).to.equal(velocity); expect(e.target).to.equal(channel); index++; if (index > 127) done(); } }); it("should report correct octave in 'noteon' event (with octaveOffset)", function (done) { // Arrange const channel = WEBMIDI_INPUT.channels[1]; const event = "noteon"; const message = [0x90, 60, 64]; WebMidi.octaveOffset = -1; WEBMIDI_INPUT.octaveOffset = -1; channel.octaveOffset = -1; const result = 4 + WebMidi.octaveOffset + WEBMIDI_INPUT.octaveOffset + channel.octaveOffset; // Act channel.addListener(event, assert); VIRTUAL_INPUT.PORT.sendMessage(message); // Assert function assert(e) { expect(e.note.octave).to.equal(result); WebMidi.octaveOffset = 0; done(); } }); it("should dispatch event for inbound 'keyaftertouch' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "keyaftertouch"; let status = 0xA0; let velocity = 73; let index = 0; channel.addListener(event, assert); // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, velocity]); } // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.rawKey).to.equal(index); expect(e.rawValue).to.equal(velocity); expect(e.target).to.equal(channel); index++; if (index > 127) done(); } }); it("should report correct octave in 'keyaftertouch' event (with octaveOffset)", function (done) { // Arrange const channel = WEBMIDI_INPUT.channels[1]; const event = "keyaftertouch"; const message = [0xA0, 60, 64]; WebMidi.octaveOffset = 1; channel.input.octaveOffset = 1; channel.octaveOffset = 1; const offset = WebMidi.octaveOffset + channel.octaveOffset + channel.input.octaveOffset; const result = 4 + offset; // Act channel.addListener(event, assert); VIRTUAL_INPUT.PORT.sendMessage(message); // Assert function assert(e) { expect(e.note.octave).to.equal(result); WebMidi.octaveOffset = 0; done(); } }); it("should dispatch event for inbound 'controlchange' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "controlchange"; let status = 0xB0; let value = 123; let index = 0; channel.addListener(event, assert); // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, value]); } // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.controller.number).to.equal(index); expect(e.rawValue).to.equal(value); expect(e.target).to.equal(channel); index++; if (index > 127) done(); } }); it("should dispatch event for all 'controlchange' numbered subtypes", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "controlchange"; let status = 0xB0; let value = 123; for (let i = 0; i <= 127; i++) { channel.addListener(`${event}-controller${i}`, e => { assert(e, i); }); } // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, value]); } // Assert function assert(e, index) { expect(e.type).to.equal(`${event}-controller${index}`); expect(e.controller.number).to.equal(index); expect(e.controller.name).to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].name); expect(e.controller.description) .to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].description); expect(e.controller.position) .to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].position); expect(e.rawValue).to.equal(value); expect(e.target).to.equal(channel); if (index === 127) done(); } }); it("should dispatch event for all 'controlchange' named subtypes", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "controlchange"; let status = 0xB0; let value = 123; for (let i = 0; i <= 127; i++) { const name = Enumerations.CONTROL_CHANGE_MESSAGES[i].name; if (name.indexOf("controller") !== 0) { channel.addListener(`${event}-` + name, e => { assert(e, i); }); } } // Act for (let i = 0; i <= 127; i++) { VIRTUAL_INPUT.PORT.sendMessage([status, i, value]); } // Assert function assert(e, index) { expect(e.type).to.equal(`${event}-` + Enumerations.CONTROL_CHANGE_MESSAGES[index].name); expect(e.controller.number).to.equal(index); expect(e.controller.name).to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].name); expect(e.controller.description) .to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].description); expect(e.controller.position) .to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[index].position); expect(e.rawValue).to.equal(value); expect(e.target).to.equal(channel); if (index === 127) done(); } }); it("should dispatch event for inbound 'program change' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "programchange"; let status = 0xC0; let value = 19; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, value]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.value).to.equal(value); expect(e.rawValue).to.equal(value); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'channel aftertouch' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "channelaftertouch"; let status = 0xD0; let value = 114; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, value]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.rawValue).to.equal(value); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'pitchbend' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "pitchbend"; let status = 0xE0; let lsb = 6; let msb = 89; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, lsb, msb]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.data[1]).to.equal(lsb); expect(e.data[2]).to.equal(msb); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'all sound off' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "allsoundoff"; let status = 0xB0; let mode = 120; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, mode, 0]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'reset all controllers' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "resetallcontrollers"; let status = 0xB0; let mode = 121; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, mode, 0]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'local control' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "localcontrol"; let status = 0xB0; let mode = 122; channel.addListener(event, assert); let index = 0; // Act VIRTUAL_INPUT.PORT.sendMessage([status, mode, 0]); VIRTUAL_INPUT.PORT.sendMessage([status, mode, 127]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); if (index === 0) { expect(e.value).to.be.false; } else if (index === 1) { expect(e.value).to.be.true; done(); } index++; } }); it("should dispatch event for inbound 'all notes off' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "allnotesoff"; let status = 0xB0; let mode = 123; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, mode, 0]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); done(); } }); it("should dispatch event for inbound 'monomode/polymode' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "monomode"; let status = 0xB0; channel.addListener(event, assert); let index = 0; // Act VIRTUAL_INPUT.PORT.sendMessage([status, 126, 0]); VIRTUAL_INPUT.PORT.sendMessage([status, 127, 0]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); if (index === 0) { expect(e.value).to.be.true; } else if (index === 1) { expect(e.value).to.be.false; done(); } index++; } }); it("should dispatch event for inbound 'omni mode on/off' MIDI message", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "omnimode"; let status = 0xB0; channel.addListener(event, assert); let index = 0; // Act VIRTUAL_INPUT.PORT.sendMessage([status, 124, 0]); VIRTUAL_INPUT.PORT.sendMessage([status, 125, 0]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.target).to.equal(channel); if (index === 0) { expect(e.value).to.be.false; } else if (index === 1) { expect(e.value).to.be.true; done(); } index++; } }); it("should dispatch general and specific events for nrpn-dataentrycoarse", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "dataentrycoarse"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 6, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch nrpn-dataentryfine", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "dataentryfine"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 38, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch nrpn-dataincrement", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "dataincrement"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 96, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch nrpn-datadecrement", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "datadecrement"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); done(); } }); it("should ignore out-of-order NRPN messages", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "datadecrement"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch rpn-dataentrycoarse", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "dataentrycoarse"; let status = 0xB0; // control change let parameter = "pitchbendrange"; let parameterMsb = 0; let parameterLsb = 0; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 6, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should dispatch rpn-dataentryfine", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "dataentryfine"; let status = 0xB0; // control change let parameter = "channelfinetuning"; let parameterMsb = 0; let parameterLsb = 1; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 38, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should dispatch rpn-dataincrement", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "dataincrement"; let status = 0xB0; // control change let parameter = "tuningbank"; let parameterMsb = 0; let parameterLsb = 4; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 96, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should dispatch rpn-datadecrement", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "datadecrement"; let status = 0xB0; // control change let parameter = "referencedistanceratio"; let parameterMsb = 0x3D; let parameterLsb = 0x06; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); channel.addListener(event, assert2); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); } function assert2(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should ignore out-of-order RPN messages", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "datadecrement"; let status = 0xB0; // control change let parameter = "referencedistanceratio"; let parameterMsb = 0x3D; let parameterLsb = 0x06; let value = 123; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, 456]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert(e) { expect(e.type).to.equal(event); expect(e.subtype).to.equal(subtype); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should dispatch nrpn-databuttonincrement (legacy)", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "databuttonincrement"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 96, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch nrpn-databuttondecrement (legacy)", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "nrpn"; let subtype = "databuttondecrement"; let status = 0xB0; // control change let parameterMsb = 12; let parameterLsb = 34; let value = 56; channel.addListener(`${event}-${subtype}`, assert1); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 99, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 98, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); done(); } }); it("should dispatch rpn-databuttonincrement (legacy)", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "databuttonincrement"; let status = 0xB0; // control change let parameter = "tuningbank"; let parameterMsb = 0; let parameterLsb = 4; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 96, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); it("should dispatch rpn-databuttondecrement (legacy)", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "rpn"; let subtype = "databuttondecrement"; let status = 0xB0; // control change let parameter = "referencedistanceratio"; let parameterMsb = 0x3D; let parameterLsb = 0x06; let value = 123; channel.addListener(`${event}-${subtype}`, assert1); // Act VIRTUAL_INPUT.PORT.sendMessage([status, 101, parameterMsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, parameterLsb]); VIRTUAL_INPUT.PORT.sendMessage([status, 97, value]); VIRTUAL_INPUT.PORT.sendMessage([status, 101, 127]); VIRTUAL_INPUT.PORT.sendMessage([status, 100, 127]); // Assert function assert1(e) { expect(e.type).to.equal(`${event}-${subtype}`); expect(e.rawValue).to.equal(value); expect(e.parameter).to.equal(parameter); done(); } }); describe("destroy()", function () { it("should set input and channel number to null", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; // Act channel.destroy(); // Assert expect(channel.input).to.be.null; expect(channel.number).to.be.null; }); it("should remove all listeners", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; channel.addListener("test", () => {}); // Act channel.destroy(); // Assert expect(channel.hasListener()).to.be.false; }); }); describe("getChannelModeByNumber()", function () { it("should return string for valid channel mode numbers", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let results = []; // Act for (let cc in Enumerations.CHANNEL_MODE_MESSAGES) { let number = Enumerations.CHANNEL_MODE_MESSAGES[cc]; results.push(channel.getChannelModeByNumber(number)); } // Assert results.forEach(result => { expect(result).to.be.a("string"); }); }); it("should return 'false' for numbers with no match", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let values = [ -1, 0, 119, 128 ]; let results = []; // Act values.forEach(value => { results.push(channel.getChannelModeByNumber(value)); }); // Assert results.forEach(result => { expect(result).to.be.false; }); }); }); describe("getCcNameByNumber()", function () { it("should throw error for invalid control change numbers", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let values = [ -1, // 120, // "test", // undefined, // NaN, // null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { channel.getCcNameByNumber(value); }).to.throw(RangeError); } }); it("should return string for valid control change numbers", function () { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let results = []; // Act for (let cc in Enumerations.CONTROL_CHANGE_MESSAGES) { let number = Enumerations.CONTROL_CHANGE_MESSAGES[cc].number; results.push(channel.getCcNameByNumber(number)); } // Assert results.forEach(result => { expect(result).to.be.a("string"); }); }); }); describe("getNoteState()", function () { it("should return correct play state", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let status = 0x90; // note on on channel 1 let event = "noteon"; let velocity = 127; let note = 64; channel.addListener(event, assert); // Act VIRTUAL_INPUT.PORT.sendMessage([status, note, velocity]); // Assert function assert() { for (let i = 0; i < 128; i++) { if (i === note) { expect(channel.getNoteState(note)).to.equal(true); expect(channel.getNoteState(new Note(i))).to.equal(true); expect(channel.getNoteState(Utilities.toNoteIdentifier(i))).to.equal(true); } else { expect(channel.getNoteState(i)).to.equal(false); expect(channel.getNoteState(new Note(i))).to.equal(false); expect(channel.getNoteState(Utilities.toNoteIdentifier(i))).to.equal(false); } } done(); } }); it("should return correct play state (with octaveOffset", function (done) { // Arrange let channel = WEBMIDI_INPUT.channels[1]; let event = "noteon"; let velocity = 127; let index = 0; let status = 0x90; WebMidi.octaveOffset = 1; WEBMIDI_INPUT.octaveOffset = 1; channel.octaveOffset = 1; let notes = [ {identifier: "C-4", number: 0}, {identifier: "C1", number: 60}, {identifier: "G6", number: 127} ]; channel.addListener(event, assert); // Act notes.forEach(note => { VIRTUAL_INPUT.PORT.sendMessage([status, note.number, velocity]); }); // Assert function assert() { index++; if (index < notes.length) return; notes.forEach(note => { expect(channel.getNoteState(new Note(note.identifier))).to.equal(true); }); done(); } }); }); }); ================================================ FILE: test/Message.test.js ================================================ const expect = require("chai").expect; const {Message, Enumerations} = require("../dist/cjs/webmidi.cjs.js"); describe("Message Object", function() { describe("constructor()", function() { it("should correctly set the type of message for system messages", function() { // Arrange let data = new Uint8Array(3); const items = [ {type: "sysex", status: 240}, {type: "timecode", status: 241}, {type: "songposition", status: 242}, {type: "songselect", status: 243}, {type: "tunerequest", status: 246}, {type: "sysexend", status: 247}, {type: "clock", status: 248}, {type: "start", status: 250}, {type: "continue", status: 251}, {type: "stop", status: 252}, {type: "activesensing", status: 254}, {type: "reset", status: 255} ]; // Act items.forEach(item => { data[0] = item.status; const message = new Message(data); expect(message.isSystemMessage).to.be.true; expect(message.isChannelMessage).to.be.false; expect(message.type).to.equal(item.type); }); }); it("should correctly set properties for channel messages", function() { // Arrange const items = [ {data: [0x8, 0, 127], type: "noteoff"}, {data: [0x9, 32, 96], type: "noteon"}, {data: [0xA, 64, 64], type: "keyaftertouch"}, {data: [0xB, 96, 32], type: "controlchange"}, {data: [0xC, 0], type: "programchange"}, {data: [0xD, 64], type: "channelaftertouch"}, {data: [0xE, 32, 64], type: "pitchbend"} ]; const channel = 10; // Act items.forEach(item => { item.data[0] = (item.data[0] << 4) + channel - 1; const message = new Message(item.data); expect(message.isSystemMessage).to.be.false; expect(message.isChannelMessage).to.be.true; expect(message.type).to.equal(item.type); expect(message.rawData).to.equal(item.data); }); }); it("should correctly set properties for sysex with basic manufacturer code", function() { // Arrange let data = new Uint8Array(6); data[0] = Enumerations.MIDI_SYSTEM_MESSAGES.sysex; // sysex data[1] = 0x42; // Korg data[2] = 1; // Some data data[3] = 2; // Some data data[4] = 3; // Some data data[5] = Enumerations.MIDI_SYSTEM_MESSAGES.sysexend; // sysex end // Act const message = new Message(data); // Assert expect(message.manufacturerId).to.have.ordered.members([data[1]]); expect(message.dataBytes).to.have.ordered.members([data[2], data[3], data[4]]); }); it("should correctly set properties for sysex with extended manufacturer code", function() { // Arrange let data = new Uint8Array(8); data[0] = Enumerations.MIDI_SYSTEM_MESSAGES.sysex; // sysex data[1] = 0; // MOTU (byte 1) data[2] = 0; // MOTU (byte 2) data[3] = 0x3B; // MOTU (byte 3) data[4] = 1; // Some data data[5] = 2; // Some data data[6] = 3; // Some data data[7] = Enumerations.MIDI_SYSTEM_MESSAGES.sysexend; // sysex end // Act const message = new Message(data); // Assert expect(message.manufacturerId).to.have.ordered.members([data[1], data[2], data[3]]); expect(message.dataBytes).to.have.ordered.members([data[4], data[5], data[6]]); }); }); }); ================================================ FILE: test/Note.test.js ================================================ const expect = require("chai").expect; const {WebMidi, Note, Utilities} = require("../dist/cjs/webmidi.cjs.js"); describe("Note Object", function() { describe("constructor()", function () { it("should return 'Note' with defaults when using number and no options", function() { // Arrange let notes = []; // Act for (let i = 0; i <= 127; i++) notes.push(new Note(i)); // Assert notes.forEach((note, i) => { const compare = Utilities.getNoteDetails(Utilities.toNoteIdentifier(i)); expect(note).to.be.an.instanceof(Note); expect(note.attack).to.equal(WebMidi.defaults.note.attack); expect(note.release).to.equal(WebMidi.defaults.note.release); expect(note.duration).to.equal(WebMidi.defaults.note.duration); expect(note.name).to.equal(compare.name); expect(note.accidental).to.equal(compare.accidental); expect(note.octave).to.equal(compare.octave); }); }); it("should return 'Note' with defaults when using identifier and no options", function() { // Arrange let values = [ "C-1", "D#0", "E##1", "Fb2", "Fbb3", "G9", ]; // Act values.forEach(assert); // Assert function assert(value) { const note = new Note(value); expect(note).to.be.an.instanceof(Note); expect(note.attack).to.equal(WebMidi.defaults.note.attack); expect(note.release).to.equal(WebMidi.defaults.note.release); expect(note.duration).to.equal(WebMidi.defaults.note.duration); expect(note.identifier).to.equal(value); expect(note.name).to.equal(Utilities.getNoteDetails(value).name); expect(note.accidental).to.equal(Utilities.getNoteDetails(value).accidental); expect(note.octave).to.equal(Utilities.getNoteDetails(value).octave); } }); it("should return 'Note' with correct props when using number and options", function() { // Arrange let notes = []; let options = {duration: 12.34, attack: 0.26, release: 0.79}; // Act for (let i = 0; i <= 127; i++) notes.push(new Note(i, options)); // Assert notes.forEach(note => { expect(note).to.be.an.instanceof(Note); expect(note.attack).to.equal(options.attack); expect(note.rawAttack).to.equal(Utilities.fromFloatTo7Bit(options.attack)); expect(note.release).to.equal(options.release); expect(note.rawRelease).to.equal(Utilities.fromFloatTo7Bit(options.release)); expect(note.duration).to.equal(options.duration); }); }); it("should return 'Note' with correct props when using identifier and options", function() { // Arrange let values = [ "C-1", "D#0", "E##1", "Fb2", "Fbb3", "G9", ]; let options = {duration: 12.34, attack: 0.13, release: 0.98}; // Act values.forEach(value => { assert(new Note(value, options)); }); // Assert function assert(note) { expect(note).to.be.an.instanceof(Note); expect(note.attack).to.equal(options.attack); expect(note.rawAttack).to.equal(Utilities.fromFloatTo7Bit(options.attack)); expect(note.release).to.equal(options.release); expect(note.rawRelease).to.equal(Utilities.fromFloatTo7Bit(options.release)); expect(note.duration).to.equal(options.duration); } }); it("should prioritize rawAttack and rawRelease when defined", function() { // Arrange const identifier = "C4"; let options = {attack: 0.12, rawAttack: 100, release: 0.98, rawRelease: 5}; // Act const note = new Note(identifier, options); // Assert expect(note.attack).to.equal(Utilities.from7bitToFloat(options.rawAttack)); expect(note.release).to.equal(Utilities.from7bitToFloat(options.rawRelease)); }); it("should ignore 'octaveOffset' for notes specified by identifier", function() { // Arrange let triplets = [ {number: 0, octaveOffset: -1, identifier: "C0"}, {number: 0, octaveOffset: 2, identifier: "C0"}, {number: 60, octaveOffset: -3, identifier: "C4"}, {number: 60, octaveOffset: 4, identifier: "C4"}, {number: 127, octaveOffset: -5, identifier: "G9"}, {number: 127, octaveOffset: 6, identifier: "G9"} ]; // Act triplets.forEach(assert); // Assert function assert(triplet) { const note = new Note(triplet.identifier, {octaveOffset: triplet.octaveOffset}); expect(note.identifier).to.equal(triplet.identifier); } }); it("should throw when using invalid value", function() { // Arrange let values = [ -1, 128, Infinity, -Infinity, "0", "127", undefined, null, "X2", "Cbbb4", "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { new Note(value); }).to.throw(); } }); it("should throw when using invalid duration", function() { // Arrange let values = [ -1, -Infinity, "test", [], -1.5, -0.6, {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { new Note("B3", {duration: value}); }).to.throw(RangeError); } }); it("should throw when using invalid attack or release", function() { // Arrange let values = [ -1, 2, -Infinity, "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { new Note("Db5", {attack: value}); }).to.throw(RangeError); expect(function() { new Note("E##5", {release: value}); }).to.throw(RangeError); } }); it("should throw when using invalid rawAttack or rawRelease", function() { // Arrange let values = [ -1, 128, -Infinity, "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { new Note("Db5", {attack: value}); }).to.throw(RangeError); expect(function() { new Note("E##5", {release: value}); }).to.throw(RangeError); } }); }); describe("getOffsetNumber()", function () { it("should use 0 if octaveOffset is invalid", function() { // Arrange let note = new Note(42); let values = [ Infinity, -Infinity, "abc", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(note.getOffsetNumber(value)).to.equal(note.number); } }); it("should use 0 if semitoneOffset is invalid", function() { // Arrange let note = new Note(42); let values = [ Infinity, -Infinity, "abc", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(note.getOffsetNumber(0, value)).to.equal(note.number); } }); it("should cap returned value at 127", function() { // Arrange let note = new Note(42); let pairs = [ {octaveOffset: 0, semitoneOffset: 1000}, {octaveOffset: 100, semitoneOffset: 0}, {octaveOffset: 20, semitoneOffset: 20}, ]; // Act pairs.forEach(assert); // Assert function assert(pair) { expect(note.getOffsetNumber(pair.octaveOffset, pair.semitoneOffset)).to.equal(127); } }); it("should return 0 for values smaller than 0", function() { // Arrange let note = new Note(42); let pairs = [ {octaveOffset: 0, semitoneOffset: -43}, {octaveOffset: -20, semitoneOffset: 0}, {octaveOffset: -20, semitoneOffset: -20}, ]; // Act pairs.forEach(assert); // Assert function assert(pair) { expect(note.getOffsetNumber(pair.octaveOffset, pair.semitoneOffset)).to.equal(0); } }); }); describe("set identifier()", function () { it("should throw error if setting to an invalid value", function() { // Arrange let note = new Note(42); let values = [ -1, 128, Infinity, -Infinity, "0", "127", undefined, null, "X2", "Cbbb4", "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.identifier = value).to.throw(Error); } }); }); describe("set attack()", function () { it("should throw error if setting to an invalid value", function() { // Arrange let note = new Note(42); let values = [ -1, 2, -Infinity, "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.attack = value).to.throw(RangeError); } }); it("should assign correct value to 'rawAttack'", function() { // Arrange let note = new Note(42); let values = [ {value: 0, rawValue: 0}, {value: 0.25, rawValue: 32}, {value: 0.5, rawValue: 64}, {value: 0.75, rawValue: 95}, {value: 1, rawValue: 127}, ]; // Act values.forEach(assert); // Assert function assert(item) { note.attack = item.value; expect(note.rawAttack).to.equal(item.rawValue); } }); }); describe("set rawAttack()", function () { it("should assign correct value to 'attack'", function() { // Arrange let note = new Note(42); let values = [ {value: 0, rawValue: 0}, {value: 0.25, rawValue: 32}, {value: 0.5, rawValue: 64}, {value: 0.75, rawValue: 95}, {value: 1, rawValue: 127}, ]; // Act values.forEach(assert); // Assert function assert(item) { note.rawAttack = item.rawValue; expect(note.attack).to.be.within(item.value - 0.005, item.value + 0.005); } }); }); describe("set release()", function () { it("should throw error if setting to an invalid value", function() { // Arrange let note = new Note(42); let values = [ -1, 2, -Infinity, "test", [], {}, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.release = value).to.throw(RangeError); } }); it("should assign correct value to 'rawRelease'", function() { // Arrange let note = new Note(42); let values = [ {value: 0, rawValue: 0}, {value: 0.25, rawValue: 32}, {value: 0.5, rawValue: 64}, {value: 0.75, rawValue: 95}, {value: 1, rawValue: 127}, ]; // Act values.forEach(assert); // Assert function assert(item) { note.release = item.value; expect(note.rawRelease).to.equal(item.rawValue); } }); }); describe("set rawRelease()", function () { it("should assign correct value to 'release'", function() { // Arrange let note = new Note(42); let values = [ {value: 0, rawValue: 0}, {value: 0.25, rawValue: 32}, {value: 0.5, rawValue: 64}, {value: 0.75, rawValue: 95}, {value: 1, rawValue: 127}, ]; // Act values.forEach(assert); // Assert function assert(item) { note.rawRelease = item.rawValue; expect(note.release).to.be.within(item.value - 0.005, item.value + 0.005); } }); }); describe("set duration()", function () { it("should throw error if invalid duration is specified", function() { // Arrange let note = new Note(42); let values = [ -1, -Infinity, "test", [], -1.5, -0.6, {}, NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.duration = value).to.throw(); } }); }); describe("set name()", function () { it("should throw error if invalid name is specified", function() { // Arrange let note = new Note(42); let values = [ -1, -Infinity, "test", "H", [], -1.5, -0.6, {}, NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.name = value).to.throw(); } }); }); describe("set accidental()", function () { it("should throw error if invalid accidental is specified", function() { // Arrange let note = new Note(42); let values = [ -1, "###", "bbb", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.accidental = value).to.throw(); } }); }); describe("set octave()", function () { it("should throw error if invalid octave is specified", function() { // Arrange let note = new Note(42); let values = [ Infinity, -Infinity, "abc", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => note.octave = value).to.throw(); } }); }); }); ================================================ FILE: test/Output.test.js ================================================ const expect = require("chai").expect; const midi = require("@julusian/midi"); const sinon = require("sinon"); const {WebMidi, Message} = require("../dist/cjs/webmidi.cjs.js"); // The virtual port is an "external" device so an input is seen as an output by WebMidi. To avoid // confusion, the naming scheme adopts WebMidi's perspective. let VIRTUAL_OUTPUT = new midi.Input(); let VIRTUAL_OUTPUT_NAME = "Virtual Output"; let WEBMIDI_OUTPUT; describe("Output Object", function() { before(function () { VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); VIRTUAL_OUTPUT.ignoreTypes(false, false, false); // enable sysex, timing & active sensing }); after(function () { VIRTUAL_OUTPUT.closePort(); }); beforeEach("Check support and enable WebMidi.js", async function () { await WebMidi.enable(); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); }); afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); }); describe("clear()", function () { it("should return 'Output' object for chaining", function() { expect(WEBMIDI_OUTPUT.clear()).to.equal(WEBMIDI_OUTPUT); }); }); describe("close()", function () { it("should close connection", async function() { // Act await WEBMIDI_OUTPUT.close(); // Assert expect(WEBMIDI_OUTPUT.connection).to.equal("closed"); }); }); describe("sendRpnDecrement()", function () { it("should trigger the channel method for all valid channels", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let rpn = "pitchbendrange"; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendRpnDecrement")); }); // Act WEBMIDI_OUTPUT.sendRpnDecrement(rpn, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(rpn, options)).to.be.true; spy.restore(); }); }); it("should return the 'Output' object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendRpnDecrement("pitchbendrange") ).to.equal(WEBMIDI_OUTPUT); }); }); describe("destroy()", function () { it("should destroy the 'Output'", async function() { // Act await WEBMIDI_OUTPUT.destroy(); // Assert try { WEBMIDI_OUTPUT.name; } catch (e) { await Promise.resolve(); } if (WEBMIDI_OUTPUT.channels.length !== 0) return Promise.reject(); if (WEBMIDI_OUTPUT.hasListener() === true) return Promise.reject(); }); }); describe("sendRpnIncrement()", function () { it("should trigger the channel method for all valid channels", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let rpn = "pitchbendrange"; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendRpnIncrement")); }); // Act WEBMIDI_OUTPUT.sendRpnIncrement(rpn, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(rpn, options)).to.be.true; spy.restore(); }); }); it("should return the 'Output' object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendRpnIncrement("pitchbendrange") ).to.equal(WEBMIDI_OUTPUT); }); }); describe("open()", function () { it("should open connection", async function() { // Act await WEBMIDI_OUTPUT.close(); await WEBMIDI_OUTPUT.open(); // Assert expect(WEBMIDI_OUTPUT.connection).to.equal("open"); }); }); describe("playNote()", function () { it("should trigger channel method for all valid channels [legacy]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; let note = 60; WEBMIDI_OUTPUT.channels.forEach(ch => spies.push(sinon.spy(ch, "playNote"))); // Act WEBMIDI_OUTPUT.playNote(note, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, options)).to.be.true; spy.restore(); }); }); it("should trigger channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; WEBMIDI_OUTPUT.channels.forEach(ch => spies.push(sinon.spy(ch, "playNote"))); // Act WEBMIDI_OUTPUT.playNote(note, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.playNote(64) ).to.equal(WEBMIDI_OUTPUT); }); it("should send immediately if no valid time is found", function (done) { // Arrange let note = 64; let sent = WebMidi.time; let timestamps = [-1, 0, -Infinity, undefined, null, WebMidi.time, NaN]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act timestamps.forEach( stamp => WEBMIDI_OUTPUT.playNote(note, {channel: 1, time: stamp}) ); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify([144, 64, 64])) { expect(WebMidi.time - sent).to.be.within(-5, 15); index++; if (index === timestamps.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } } }); it("should properly send note off according to specified relative time", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let delay = 50; let timestamp = "+" + delay; let duration = 50; let sent = WebMidi.time; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.playNote(note, {channels: channel, time: timestamp, duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - sent - delay - duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly send note off according to specified absolute time", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let delay = 50; let timestamp = WebMidi.time + delay; let duration = 50; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.playNote(note, {channels: channel, time: timestamp, duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - timestamp - duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly send note off when no time is specified", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let duration = 50; let sent = WebMidi.time; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.playNote(note, {channels: channel, duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - sent - duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); }); describe("sendResetAllControllers()", function () { it("should trigger channel method for all valid channels [legacy]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendResetAllControllers")); }); // Act WEBMIDI_OUTPUT.sendResetAllControllers(valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should trigger channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendResetAllControllers")); }); // Act WEBMIDI_OUTPUT.sendResetAllControllers(options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendResetAllControllers() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("send()", function () { it("should throw error if 'status' byte is invalid (legacy)", function() { // Arrange let values = ["xxx", NaN, 127, 256, undefined, null, -1, 0, {}]; // Act values.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(value); }).to.throw(RangeError); }; }); it("should throw error if 'status' byte is invalid", function() { // Arrange const uint8Array1 = new Uint8Array(1); uint8Array1[0] = 127; const uint8Array2 = new Uint8Array(1); uint8Array2[0] = 0; let messages = [ ["xxx"], [NaN], [127], [256], [undefined], [null], [-1], [0], [{}], uint8Array1, uint8Array2 ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(value); }).to.throw(RangeError); }; }); it("should throw error if 'data' bytes are invalid (legacy)", function() { // Arrange let values = [ ["xxx"], [-1], [256], [NaN], [null], [Infinity] ]; // Act values.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(0x90, value); }).to.throw(RangeError); }; }); it("should throw error if 'data' bytes are invalid", function() { // Arrange let messages = [ [0x90, "xxx"], [0x90, -1], [0x90, 256], [0x90, NaN], [0x90, null], [0x90, Infinity] ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(value); }).to.throw(RangeError); }; }); it("should throw error if message is incomplete (legacy)", function() { // Arrange let values = [ undefined, NaN, [] ]; // Act values.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(0x90, value); }).to.throw(TypeError); }; }); it("should throw error if message is incomplete", function() { // Arrange const uint8Array = new Uint8Array(1); uint8Array[0] = 0x90; let messages = [ [0x90], uint8Array ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.send(value); }).to.throw(TypeError); }; }); it("should return the Output object for method chaining (legacy)", function() { expect( WEBMIDI_OUTPUT.send(144, [64, 64]) ).to.equal(WEBMIDI_OUTPUT); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.send([144, 64, 64]) ).to.equal(WEBMIDI_OUTPUT); }); it("should actually send the message defined by array (legacy)", function(done) { // Arrange let expected = [0x90, 60, 127]; // Note on: channel 0 (144), note number (60), velocity (127) VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send( expected[0], [expected[1], expected[2]] ); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should actually send the message defined by array (normal)", function(done) { // Arrange let message = [0x90, 60, 127]; // Note on: channel 0 (144), note number (60), velocity (127) VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(message); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(message); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should actually send the message defined by Uint8Array (normal)", function(done) { // Arrange const data = [0x90, 60, 127]; const uint8array = Uint8Array.from(data); // Note on + channel 0, note 60, velocity (127) VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(uint8array); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(data); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should actually send the message defined by Message (normal)", function(done) { // Arrange const data = Uint8Array.from([0x90, 60, 127]); // Note on + ch. 1, number (60), velocity (127) const message = new Message(data); VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(message); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(message); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should send immediately if no valid timestamp is found (legaqy) ", function (done) { // Arrange let status = 144; let data = [13, 0]; let sent = WebMidi.time; let timestamps = [ -1, 0, -Infinity, undefined, null, NaN ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act timestamps.forEach( stamp => WEBMIDI_OUTPUT.send(status, data, stamp) ); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify([].concat(status, data))) { expect(WebMidi.time - sent).to.be.within(0, 5); index++; if (index === timestamps.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } } }); it("should send immediately if no valid timestamp is found", function (done) { // Arrange let data = [144, 13, 0]; let sent = WebMidi.time; let timestamps = [ {time: -1}, {time: -Infinity}, {time: undefined}, {time: null}, {time: NaN}, {} ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act timestamps.forEach( stamp => WEBMIDI_OUTPUT.send(data, stamp) ); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(data)) { expect(WebMidi.time - sent).to.be.within(0, 5); index++; if (index === timestamps.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } } }); it("should schedule message according to absolute timestamp (legacy)", function (done) { // Arrange let status = 144; let data = [10, 0]; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(status, data, target); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); it("should schedule message according to absolute timestamp", function (done) { // Arrange let message = [144, 10, 0]; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(message, {time: target}); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); it("should schedule message according to relative timestamp (legacy)", function (done) { // Arrange let status = 144; let data = [10, 0]; let offset = "+100"; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(status, data, offset); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); it("should schedule message according to relative timestamp", function (done) { // Arrange let message = [144, 10, 0]; let offset = "+100"; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.send(message, {time: offset}); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); }); describe("sendActiveSensing()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 254) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (254) WEBMIDI_OUTPUT.sendActiveSensing(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendActiveSensing(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendChannelAftertouch() [legacy]", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendChannelAftertouch")); }); WEBMIDI_OUTPUT.sendChannelAftertouch(value, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendChannelAftertouch(0.5) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendChannelMode()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendChannelMode")); }); WEBMIDI_OUTPUT.sendChannelMode("allnotesoff", 42, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly("allnotesoff", 42, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 42; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendChannelMode")); }); // Act WEBMIDI_OUTPUT.sendChannelMode("allnotesoff", value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly("allnotesoff", value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendChannelMode("allnotesoff", 42, [1]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendClock()", function () { it("should actually send the message", function(done) { // Arrange VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.sendClock(); // Assert function assert(deltaTime, message) { if (message[0] === 248) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendClock() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendContinue()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 251) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (251) WEBMIDI_OUTPUT.sendContinue(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendContinue() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendControlChange()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendControlChange")); }); WEBMIDI_OUTPUT.sendControlChange(60, 64, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, 64, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; let value = 64; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendControlChange")); }); // Act WEBMIDI_OUTPUT.sendControlChange(note, value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendControlChange(60, 64, [1]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendKeyAftertouch()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let options = {time: 123, channels: valid}; let pressure = 0.75; let note = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendKeyAftertouch")); }); WEBMIDI_OUTPUT.sendKeyAftertouch(note, pressure, options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, pressure, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendKeyAftertouch(60, 0.75) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendNoteOff()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNoteOff")); }); WEBMIDI_OUTPUT.sendNoteOff(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNoteOff")); }); // Act WEBMIDI_OUTPUT.sendNoteOff(note, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendNoteOff(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendNoteOn()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNoteOn")); }); WEBMIDI_OUTPUT.sendNoteOn(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNoteOn")); }); // Act WEBMIDI_OUTPUT.sendNoteOn(note, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(note, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendNoteOn(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendPitchBend() [legacy]", function () { it("should trigger the channel method for all valid channels", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; let bend = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPitchBend")); }); // Act WEBMIDI_OUTPUT.sendPitchBend(bend, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(bend, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { // Arrange let bend = 0.5; let channel = 1; // Assert expect( WEBMIDI_OUTPUT.sendPitchBend(bend, channel) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendProgramChange() [legacy]", function () { it("should trigger the channel method for all valid channels", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendProgramChange")); }); WEBMIDI_OUTPUT.sendProgramChange(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendProgramChange(64, [1]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendReset()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 255) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (255) WEBMIDI_OUTPUT.sendReset(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendReset(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendSongPosition() [legacy]", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 242) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (242) WEBMIDI_OUTPUT.sendSongPosition(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendSongPosition(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendSongSelect() [legacy]", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 243) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (243) WEBMIDI_OUTPUT.sendSongSelect(42); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendSongSelect(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendStart()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 250) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (250) WEBMIDI_OUTPUT.sendStart(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendStart(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendStop()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 252) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (252) WEBMIDI_OUTPUT.sendStop(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendStop(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendSysex()", function () { it("should throw error if 'sysex' was not enabled", function() { expect(() => { WEBMIDI_OUTPUT.sendSysex(66, [1, 2, 3, 4, 5]); }).to.throw(); }); it("should actually send the message if defined by array", async function() { await WebMidi.disable(); await WebMidi.enable({sysex: true}); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); await new Promise(resolve => { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if ( message[0] === 240 && // sysex star byte message[1] === 0x42 && // Korg message[2] === 0x1 && message[3] === 0x2 && message[4] === 0x3 && message[5] === 247 // sysex end byte ) { VIRTUAL_OUTPUT.removeAllListeners(); resolve(); } }); // Sysex (240...247) WEBMIDI_OUTPUT.sendSysex(0x42, [0x1, 0x2, 0x3]); }); }); it("should actually send the message if defined by Uint8Array", async function() { // Arrange await WebMidi.disable(); await WebMidi.enable({sysex: true}); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); const data = Uint8Array.from([0x1, 0x2, 0x3]); // Act await new Promise(resolve => { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if ( message[0] === 240 && // sysex star byte message[1] === 0x42 && // Korg message[2] === 0x1 && message[3] === 0x2 && message[4] === 0x3 && message[5] === 247 // sysex end byte ) { VIRTUAL_OUTPUT.removeAllListeners(); resolve(); } }); // Sysex (240...247) WEBMIDI_OUTPUT.sendSysex(0x42, data); }); }); it("should return the Output object for method chaining", async function() { await WebMidi.disable(); await WebMidi.enable({sysex: true}); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); expect( WEBMIDI_OUTPUT.sendSysex(66, [1, 2, 3, 4, 5]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendTimecodeQuarterFrame()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 241 && message[1] === 42) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (251) WEBMIDI_OUTPUT.sendTimecodeQuarterFrame(42); }); it("should throw error on invalid data", function() { [255, 9999999, undefined, null, [], {}].forEach(value => { expect(() => { WEBMIDI_OUTPUT.sendTimecodeQuarterFrame(value); }).to.throw(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendTimecodeQuarterFrame(0) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendTuneRequest()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 246) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (246) WEBMIDI_OUTPUT.sendTuneRequest(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendTuneRequest() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendTuningRequest() [legacy]", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 246) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (246) WEBMIDI_OUTPUT.sendTuningRequest(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendTuningRequest() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendChannelAftertouch()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendChannelAftertouch")); }); // Act WEBMIDI_OUTPUT.sendChannelAftertouch(value, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendChannelAftertouch")); }); // Act WEBMIDI_OUTPUT.sendChannelAftertouch(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { // Arrange let value = 0.5; // Assert expect( WEBMIDI_OUTPUT.sendChannelAftertouch(value) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendKeyAftertouch()", function () { it("should trigger the channel method for all valid channels", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendKeyAftertouch")); }); // Act WEBMIDI_OUTPUT.sendKeyAftertouch(note, value, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWith(note, value)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let note = 60; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendKeyAftertouch")); }); // Act WEBMIDI_OUTPUT.sendKeyAftertouch(note, value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWith(note, value)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { // Arrange let note = 60; let value = 0.5; // Assert expect( WEBMIDI_OUTPUT.sendKeyAftertouch(note, value) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendLocalControl()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendLocalControl")); }); WEBMIDI_OUTPUT.sendLocalControl(true, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(true, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendLocalControl")); }); // Act WEBMIDI_OUTPUT.sendLocalControl(true, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(true, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendLocalControl(true) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendMasterTuning()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendMasterTuning")); }); WEBMIDI_OUTPUT.setMasterTuning(42, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(42, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 42; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendMasterTuning")); }); // Act WEBMIDI_OUTPUT.sendMasterTuning(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendMasterTuning(true) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendModulationRange()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendModulationRange")); }); // Act WEBMIDI_OUTPUT.setModulationRange(42, 24, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(42, 24, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let semitones = 42; let cents = 24; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendModulationRange")); }); // Act WEBMIDI_OUTPUT.sendModulationRange(semitones, cents, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(semitones, cents, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendModulationRange(5) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendNrpnValue()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNrpnValue")); }); WEBMIDI_OUTPUT.setNonRegisteredParameter([2, 63], [0, 10], valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly([2, 63], [0, 10], options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let parameter = [2, 63]; let data = [0, 10]; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendNrpnValue")); }); // Act WEBMIDI_OUTPUT.sendNrpnValue(parameter, data, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(parameter, data, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendNrpnValue([2, 63], [0, 10]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendOmniMode()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendOmniMode")); }); WEBMIDI_OUTPUT.sendOmniMode(true, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(true, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = true; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendOmniMode")); }); // Act WEBMIDI_OUTPUT.sendOmniMode(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendOmniMode(true) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendProgramChange()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendProgramChange")); }); WEBMIDI_OUTPUT.sendProgramChange(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendProgramChange")); }); // Act WEBMIDI_OUTPUT.sendProgramChange(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendProgramChange(64, [1]) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendPitchBend()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPitchBend")); }); // Act WEBMIDI_OUTPUT.sendPitchBend(0.5, valid.concat(invalid), options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(0.5, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 0.5; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPitchBend")); }); // Act WEBMIDI_OUTPUT.sendPitchBend(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { // Arrange let value = -0.5; // Assert expect( WEBMIDI_OUTPUT.sendPitchBend(value) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendPitchBendRange()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPitchBendRange")); }); WEBMIDI_OUTPUT.setPitchBendRange(42, 24, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(42, 24, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let semitones = 42; let cents = 24; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPitchBendRange")); }); // Act WEBMIDI_OUTPUT.sendPitchBendRange(semitones, cents, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(semitones, cents, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendPitchBendRange(42, 24) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendPolyphonicMode()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPolyphonicMode")); }); WEBMIDI_OUTPUT.sendPolyphonicMode("mono", valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly("mono", options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = "mono"; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendPolyphonicMode")); }); // Act WEBMIDI_OUTPUT.sendPolyphonicMode(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendPolyphonicMode("mono") ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendProgramChange()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendProgramChange")); }); WEBMIDI_OUTPUT.sendProgramChange(42, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(42, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 42; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendProgramChange")); }); WEBMIDI_OUTPUT.sendProgramChange(value, options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendProgramChange(42) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendRpnValue()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendRpnValue")); }); WEBMIDI_OUTPUT.setRegisteredParameter("modulationrange", 42, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly("modulationrange", 42, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let parameter = "modulationrange"; let value = 42; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendRpnValue")); }); WEBMIDI_OUTPUT.sendRpnValue(parameter, value, options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(parameter, value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendRpnValue("modulationrange", 42) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendSongSelect()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 243) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (243) WEBMIDI_OUTPUT.sendSongSelect(42); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendSongSelect(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendSongPosition()", function () { it("should actually send the message", function(done) { VIRTUAL_OUTPUT.on("message", (deltaTime, message) => { if (message[0] === 242) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // Note on (242) WEBMIDI_OUTPUT.sendSongPosition(); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendActiveSensing(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendTuningBank()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendTuningBank")); }); WEBMIDI_OUTPUT.setTuningBank(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let bank = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendTuningBank")); }); // Act WEBMIDI_OUTPUT.sendTuningBank(bank, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(bank, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendTuningBank(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendTuningProgram()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendTuningProgram")); }); WEBMIDI_OUTPUT.setTuningProgram(60, valid.concat(invalid), options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(60, options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; let value = 60; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendTuningProgram")); }); // Act WEBMIDI_OUTPUT.sendTuningProgram(value, options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(value, options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendTuningProgram(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("stopNote()", function () { it("should call 'sendNoteOff()' method", function() { // Arrange let note = 60; let options = {time: 0}; let spy = sinon.spy(WEBMIDI_OUTPUT, "sendNoteOff"); // Act WEBMIDI_OUTPUT.stopNote(note, options); // Assert expect(spy.calledOnceWithExactly(note, options)).to.be.true; }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.stopNote(64) ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendAllNotesOff()", function () { it("should trigger the channel method for all valid channels", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendAllNotesOff")); }); WEBMIDI_OUTPUT.sendAllNotesOff(options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendAllNotesOff")); }); // Act WEBMIDI_OUTPUT.sendAllNotesOff(options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendAllNotesOff() ).to.equal(WEBMIDI_OUTPUT); }); }); describe("sendAllSoundOff()", function () { it("should trigger the channel method for all valid channels [legacy]", function() { let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendAllSoundOff")); }); WEBMIDI_OUTPUT.sendAllSoundOff(options); spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should trigger the channel method for all valid channels [normal]", function() { // Arrange let spies = []; let valid = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let invalid = [undefined, null, NaN, -1, "", Infinity, -Infinity]; let options = {time: 123, channels: valid.concat(invalid)}; WEBMIDI_OUTPUT.channels.forEach(ch => { spies.push(sinon.spy(ch, "sendAllSoundOff")); }); // Act WEBMIDI_OUTPUT.sendAllSoundOff(options); // Assert spies.forEach(spy => { expect(spy.calledOnceWithExactly(options)).to.be.true; spy.restore(); }); }); it("should return the Output object for method chaining", function() { expect( WEBMIDI_OUTPUT.sendAllSoundOff() ).to.equal(WEBMIDI_OUTPUT); }); }); }); ================================================ FILE: test/OutputChannel.test.js ================================================ const expect = require("chai").expect; const midi = require("@julusian/midi"); const sinon = require("sinon"); const {WebMidi, Note, Utilities, Message, Enumerations} = require("../dist/cjs/webmidi.cjs.js"); // The virtual port is an "external" device so an input is seen as an output by WebMidi. To avoid // confusion, the naming scheme adopts WebMidi's perspective. let VIRTUAL_OUTPUT = new midi.Input(); let VIRTUAL_OUTPUT_NAME = "Virtual Output"; let WEBMIDI_OUTPUT; /** * Caution: the tests below are executed against the "development" version of the library. The * development version, throws more errors than the production version for performance reasons. */ describe("OutputChannel Object", function() { before(function () { VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); VIRTUAL_OUTPUT.ignoreTypes(false, false, false); // enable sysex, timing & active sensing }); after(function () { VIRTUAL_OUTPUT.closePort(); }); beforeEach("Check support and enable", async function () { await WebMidi.enable(); WEBMIDI_OUTPUT = WebMidi.getOutputByName(VIRTUAL_OUTPUT_NAME); }); afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); }); describe("sendRpnDecrement()", function () { it("should throw error if parameter, specified by name, is invalid", function () { // Arrange let invalid = ["xxx", undefined, null, ""]; // Act invalid.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnDecrement(value); }).to.throw(TypeError); } }); it("should throw error if parameter, specified by array, is invalid", function () { // Arrange let values = [ [], [-1, -1], [0x3d, 1111], undefined, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnDecrement(value); }).to.throw(); }; }); it("should send correct MIDI messages for all valid named parameters", function(done) { // Arrange let index = 0; let parameters = [ {name: "pitchbendrange", value: [0x00, 0x00]}, {name: "channelfinetuning", value: [0x00, 0x01]}, {name: "channelcoarsetuning", value: [0x00, 0x02]}, {name: "tuningprogram", value: [0x00, 0x03]}, {name: "tuningbank", value: [0x00, 0x04]}, {name: "modulationrange", value: [0x00, 0x05]}, {name: "azimuthangle", value: [0x3D, 0x00]}, {name: "elevationangle", value: [0x3D, 0x01]}, {name: "gain", value: [0x3D, 0x02]}, {name: "distanceratio", value: [0x3D, 0x03]}, {name: "maximumdistance", value: [0x3D, 0x04]}, {name: "maximumdistancegain", value: [0x3D, 0x05]}, {name: "referencedistanceratio", value: [0x3D, 0x06]}, {name: "panspreadangle", value: [0x3D, 0x07]}, {name: "rollangle", value: [0x3D, 0x08]}, ]; let expected = []; parameters.forEach(param => { expected.push([176, 101, param.value[0]]); // select rpn expected.push([176, 100, param.value[1]]); // select rpn expected.push([176, 97, 0]); // decrement expected.push([176, 101, 127]); // deselect rpn expected.push([176, 100, 127]); // deselect rpn }); VIRTUAL_OUTPUT.on("message", assert); // Act parameters.forEach(param => { WEBMIDI_OUTPUT.channels[1].sendRpnDecrement(param.name); }); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI messages for all valid array parameters", function(done) { // Arrange let index = 0; let parameters = [ {name: "pitchbendrange", value: [0x00, 0x00]}, {name: "channelfinetuning", value: [0x00, 0x01]}, {name: "channelcoarsetuning", value: [0x00, 0x02]}, {name: "tuningprogram", value: [0x00, 0x03]}, {name: "tuningbank", value: [0x00, 0x04]}, {name: "modulationrange", value: [0x00, 0x05]}, {name: "azimuthangle", value: [0x3D, 0x00]}, {name: "elevationangle", value: [0x3D, 0x01]}, {name: "gain", value: [0x3D, 0x02]}, {name: "distanceratio", value: [0x3D, 0x03]}, {name: "maximumdistance", value: [0x3D, 0x04]}, {name: "maximumdistancegain", value: [0x3D, 0x05]}, {name: "referencedistanceratio", value: [0x3D, 0x06]}, {name: "panspreadangle", value: [0x3D, 0x07]}, {name: "rollangle", value: [0x3D, 0x08]}, ]; let expected = []; parameters.forEach(param => { expected.push([176, 101, param.value[0]]); // select rpn expected.push([176, 100, param.value[1]]); // select rpn expected.push([176, 97, 0]); // decrement expected.push([176, 101, 127]); // deselect rpn expected.push([176, 100, 127]); // deselect rpn }); VIRTUAL_OUTPUT.on("message", assert); // Act parameters.forEach(param => { WEBMIDI_OUTPUT.channels[1].sendRpnDecrement(param.value); }); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly call '_selectRegisteredParameter()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_selectRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnDecrement("pitchbendrange", options); // Assert expect( spy.calledOnceWithExactly( Enumerations.REGISTERED_PARAMETERS["pitchbendrange"], options ) ).to.be.true; }); it("should properly call 'sendControlChange()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendControlChange"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnDecrement("pitchbendrange", options); // Assert expect( spy.calledWithExactly(0x61, 0, options) ).to.be.true; }); it("should properly call '_deselectRegisteredParameter()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_deselectRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnDecrement("pitchbendrange", options); // Assert expect( spy.calledOnceWithExactly(options) ).to.be.true; }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendRpnDecrement("pitchbendrange") ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("destroy()", function () { it("should set output and channel number to null", function () { // Act WEBMIDI_OUTPUT.channels[1].destroy(); // Assert expect(WEBMIDI_OUTPUT.channels[1].output).to.be.null; expect(WEBMIDI_OUTPUT.channels[1].number).to.be.null; }); it("should remove all listeners", function () { // Arrange WEBMIDI_OUTPUT.channels[1].addListener("test", () => {}); // Act WEBMIDI_OUTPUT.channels[1].destroy(); // Assert expect(WEBMIDI_OUTPUT.channels[1].hasListener()).to.be.false; }); }); describe("sendRpnIncrement()", function () { it("should throw error if registered parameter, specified by name, is invalid", function () { // Arrange let values = ["xxx", undefined, null, ""]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnIncrement(value); }).to.throw(TypeError); } }); it("should throw error if registered parameter, specified by array, is invalid", function () { // Arrange let values = [ [], [-1, -1], [0x3d, 1111] ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnIncrement(value); }).to.throw(); }; }); it("should send correct MIDI messages for all valid named parameters", function(done) { // Arrange let index = 0; let parameters = [ {name: "pitchbendrange", value: [0x00, 0x00]}, {name: "channelfinetuning", value: [0x00, 0x01]}, {name: "channelcoarsetuning", value: [0x00, 0x02]}, {name: "tuningprogram", value: [0x00, 0x03]}, {name: "tuningbank", value: [0x00, 0x04]}, {name: "modulationrange", value: [0x00, 0x05]}, {name: "azimuthangle", value: [0x3D, 0x00]}, {name: "elevationangle", value: [0x3D, 0x01]}, {name: "gain", value: [0x3D, 0x02]}, {name: "distanceratio", value: [0x3D, 0x03]}, {name: "maximumdistance", value: [0x3D, 0x04]}, {name: "maximumdistancegain", value: [0x3D, 0x05]}, {name: "referencedistanceratio", value: [0x3D, 0x06]}, {name: "panspreadangle", value: [0x3D, 0x07]}, {name: "rollangle", value: [0x3D, 0x08]}, ]; let expected = []; parameters.forEach(param => { expected.push([176, 101, param.value[0]]); // select rpn expected.push([176, 100, param.value[1]]); // select rpn expected.push([176, 96, 0]); // increment expected.push([176, 101, 127]); // deselect rpn expected.push([176, 100, 127]); // deselect rpn }); VIRTUAL_OUTPUT.on("message", assert); // Act parameters.forEach(param => { WEBMIDI_OUTPUT.channels[1].sendRpnIncrement(param.name); }); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI messages for all valid array parameters", function(done) { // Arrange let index = 0; let parameters = [ {name: "pitchbendrange", value: [0x00, 0x00]}, {name: "channelfinetuning", value: [0x00, 0x01]}, {name: "channelcoarsetuning", value: [0x00, 0x02]}, {name: "tuningprogram", value: [0x00, 0x03]}, {name: "tuningbank", value: [0x00, 0x04]}, {name: "modulationrange", value: [0x00, 0x05]}, {name: "azimuthangle", value: [0x3D, 0x00]}, {name: "elevationangle", value: [0x3D, 0x01]}, {name: "gain", value: [0x3D, 0x02]}, {name: "distanceratio", value: [0x3D, 0x03]}, {name: "maximumdistance", value: [0x3D, 0x04]}, {name: "maximumdistancegain", value: [0x3D, 0x05]}, {name: "referencedistanceratio", value: [0x3D, 0x06]}, {name: "panspreadangle", value: [0x3D, 0x07]}, {name: "rollangle", value: [0x3D, 0x08]}, ]; let expected = []; parameters.forEach(param => { expected.push([176, 101, param.value[0]]); // select rpn expected.push([176, 100, param.value[1]]); // select rpn expected.push([176, 96, 0]); // increment expected.push([176, 101, 127]); // deselect rpn expected.push([176, 100, 127]); // deselect rpn }); VIRTUAL_OUTPUT.on("message", assert); // Act parameters.forEach(param => { WEBMIDI_OUTPUT.channels[1].sendRpnIncrement(param.value); }); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly call '_selectRegisteredParameter()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_selectRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnIncrement("pitchbendrange", options); // Assert expect( spy.calledOnceWithExactly( Enumerations.REGISTERED_PARAMETERS["pitchbendrange"], options ) ).to.be.true; }); it("should properly call 'sendControlChange()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendControlChange"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnIncrement("pitchbendrange", options); // Assert expect( spy.calledWithExactly(0x60, 0, options) ).to.be.true; }); it("should properly call '_deselectRegisteredParameter()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_deselectRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnIncrement("pitchbendrange", options); // Assert expect( spy.calledOnceWithExactly(options) ).to.be.true; }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendRpnIncrement("pitchbendrange") ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("playNote()", function () { it("should call 'sendNoteOn()' with correct parameters", function () { // Arrange let note = "G5"; let options = {time: 10, attack: 0.5, rawAttack: 127}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOn"); // Act WEBMIDI_OUTPUT.channels[1].playNote(note, options); // Assert expect(spy.calledOnceWith(note)).to.be.true; expect(spy.args[0][0]).to.equal(note); expect(spy.args[0][1].time).to.equal(options.time); expect(spy.args[0][1].attack).to.equal(options.attack); expect(spy.args[0][1].rawAttack).to.equal(options.rawAttack); }); it("should call 'sendNoteOff()' with correct parameters if duration is valid", function () { // Arrange let note = "G5"; let options = {time: 10, duration: 20, release: 0.5, rawRelease: 127}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // Act WEBMIDI_OUTPUT.channels[1].playNote(note, options); // Assert expect(spy.calledOnceWith(note)).to.be.true; expect(spy.args[0][0]).to.equal(note); expect(spy.args[0][1].time).to.equal(options.time + options.duration); expect(spy.args[0][1].release).to.equal(options.release); expect(spy.args[0][1].rawRelease).to.equal(options.rawRelease); }); it("shouldn't call 'sendNoteOff()' if duration is invalid", function () { // Arrange let values = [0, undefined, null, "", NaN, Infinity, -Infinity, -1]; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // Act values.forEach(value => { WEBMIDI_OUTPUT.channels[1].playNote("C3", {duration: value}); }); // Assert expect(spy.callCount).to.equal(0); }); it("should call 'sendNoteOn()' with correct parameters if passed a Note", function () { // Arrange let note = new Note("C4", { duration: 250, attack: 0.5, rawAttack: 127 }); let options = { time: 10 }; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // Act WEBMIDI_OUTPUT.channels[1].playNote(note, options); // Assert expect(spy.calledOnceWith(note)).to.be.true; expect(spy.args[0][0]).to.equal(note); expect(spy.args[0][0].attack).to.equal(note.attack); expect(spy.args[0][0].rawAttack).to.equal(note.rawAttack); expect(spy.args[0][1].time).to.equal(options.time + note.duration); }); it("should call 'sendNoteOff()' for each note if parameter is Note[]", function () { // Arrange let note = new Note("C4", { duration: 250 }); let notes = [ note, new Note("D#2", { duration: 500 }), new Note("F3", { duration: 250 }) ]; let options = {}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // // Act WEBMIDI_OUTPUT.channels[1].playNote(notes, options); // // Assert expect(spy.callCount).to.equal(3); expect(spy.args[0][0]).to.equal(note); // Hard to get exact timing values, assert in future expect(spy.args[0][1].time).to.greaterThan(WebMidi.time); expect(spy.args[0][1].release).to.equal(note.release); expect(spy.args[0][1].rawRelease).to.equal(note.rawRelease); }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].playNote("C3") ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); it("should properly send note off according to specified relative time", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let delay = 100; let timestamp = "+" + delay; let duration = 100; let sent = WebMidi.time; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].playNote(note, {time: timestamp, duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - sent - delay - duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeListener("message", assert); done(); } } }); it("should properly send note off according to specified absolute time", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let delay = 100; let timestamp = WebMidi.time + delay; let duration = 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].playNote(note, {time: timestamp, duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - timestamp - duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeListener("message", assert); done(); } } }); it("should properly send note off when no time is specified", function (done) { // Arrange let note = 64; let channel = 1; let expected = [128, note, 64]; // 128 = note off on ch 1 let duration = 100; let sent = WebMidi.time; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].playNote(note, {duration: duration}); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - sent - duration).to.be.within(-5, 15); VIRTUAL_OUTPUT.removeListener("message", assert); done(); } } }); it("should call 'sendNoteOff()' if duration in Note object is valid", function (done) { // Arrange let note = new Note("C4", { duration: 250 }); let channel = WEBMIDI_OUTPUT.channels[1]; let expected = [128, note.number, 64]; // 128 = note off on ch 1 let sent = WebMidi.time; VIRTUAL_OUTPUT.on("message", assert); // Act channel.playNote(note); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(expected)) { expect(WebMidi.time - sent - note.duration).to.be.within(-5, 10); VIRTUAL_OUTPUT.removeListener("message", assert); done(); } } }); }); describe("sendResetAllControllers()", function () { it("should properly call 'sendChannelMode()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendResetAllControllers(options); // Assert expect(spy.calledOnceWithExactly("resetallcontrollers", 0, options)).to.be.true; }); it("should send correct MIDI message", function(done) { // Arrange let expected = [176, 121, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendResetAllControllers(); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeListener("message", assert); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendResetAllControllers() ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("send()", function () { it("should throw error when invalid status is provided", function () { // Arrange const uint8Array1 = new Uint8Array(1); uint8Array1[0] = 127; const uint8Array2 = new Uint8Array(1); uint8Array2[0] = 0; let messages = [ ["xxx"], [NaN], [127], [256], [undefined], [null], [-1], [0], [{}], uint8Array1, uint8Array2 ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.channels[1].send(value); }).to.throw(RangeError); }; }); it("should throw error when invalid data is provided", function () { // Arrange let messages = [ [0x90, "xxx"], [0x90, -1], [0x90, 256], [0x90, NaN], [0x90, null], [0x90, Infinity] ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.channels[1].send(value); }).to.throw(RangeError); }; }); it("should actually send MIDI message specified by array", function (done) { // Arrange let message = [0x90, 60, 127]; // Note on: channel 0 (144), note number (60), velocity (127) VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].send(message); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(message); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); // We cannot test sending with Uint8Array because it is not supported in Node.js it("should actually send MIDI message specified by Uint8Array"); it("should actually send MIDI message specified by Message object", function (done) { // Arrange let data = Uint8Array.from([0x90, 60, 127]); // Note on: ch 0, number (60), velocity (127) const message = new Message(data); VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].send(message); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(message); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should throw error if message is incomplete", function() { // Arrange const uint8Array = new Uint8Array(1); uint8Array[0] = 0x90; let messages = [ [0x90], uint8Array ]; // Act messages.forEach(assert); // Assert function assert(value){ expect(() => { WEBMIDI_OUTPUT.channels[1].send(value); }).to.throw(TypeError); }; }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].send([144, 127, 127]) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); it("should send immediately if no valid timestamp is found", function (done) { // Arrange let data = [144, 13, 0]; let sent = WebMidi.time; let timestamps = [ {time: -1}, {time: -Infinity}, {time: undefined}, {time: null}, {time: NaN}, {} ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act timestamps.forEach( stamp => WEBMIDI_OUTPUT.channels[1].send(data, stamp) ); // Assert function assert(deltaTime, message) { if (JSON.stringify(message) == JSON.stringify(data)) { expect(WebMidi.time - sent).to.be.within(0, 5); index++; if (index === timestamps.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } } }); it("should schedule message according to absolute timestamp", function (done) { // Arrange let message = [144, 10, 0]; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].send(message, {time: target}); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); it("should schedule message according to relative timestamp", function (done) { // Arrange let message = [144, 10, 0]; let offset = "+100"; let target = WebMidi.time + 100; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].send(message, {time: offset}); // Assert function assert() { VIRTUAL_OUTPUT.removeAllListeners(); expect(WebMidi.time - target).to.be.within(-5, 10); done(); } }); }); describe("sendChannelMode()", function () { it("should properly send MIDI message when using message numbers", function(done) { // Arrange let index = 120; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 120; i < 128; i++) WEBMIDI_OUTPUT.channels[1].sendChannelMode(i, 0); index = 120; // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members([176, index, 0]); index++; if (index >= 128) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should actually send MIDI message when using message names", function(done) { // Arrange let index = 120; let names = [ "allsoundoff", "resetallcontrollers", "localcontrol", "allnotesoff", "omnimodeoff", "omnimodeon", "monomodeon", "polymodeon" ]; VIRTUAL_OUTPUT.on("message", assert); // Act names.forEach(name => WEBMIDI_OUTPUT.channels[1].sendChannelMode(name, 0)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members([176, index, 0]); index++; if (index >= 128) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid name is passed", function () { // Arrange let values = [ "xxx", 0, 1, -1, undefined, Infinity, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendChannelMode(value, 0); }).to.throw(); } }); it("should throw error when invalid value is passed", function () { // Arrange let values = [ -1, 128, NaN, null, Infinity, -Infinity ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendChannelMode(120, value); }).to.throw(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendChannelMode(120, 0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendControlChange()", function () { it("should properly send MIDI message when using controller numbers", function(done) { // Arrange let index = 0; const max = 127; // Act VIRTUAL_OUTPUT.on("message", assert); for (let i = 0; i <= max; i++) WEBMIDI_OUTPUT.channels[1].sendControlChange(i, 123); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(176); expect(message[1]).to.equal(index); index++; if (index > max) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly send 2 MIDI message when using value array", function(done) { // Arrange let index = 0; // Act VIRTUAL_OUTPUT.on("message", assert); WEBMIDI_OUTPUT.channels[1].sendControlChange(0, [12, 34]); // Assert function assert(deltaTime, message) { if (index === 0) { expect(message[0]).to.equal(0xB0); // control change on channel 1 expect(message[1]).to.equal(0); // bankselectcoarse expect(message[2]).to.equal(12); // bankselectcoarse index++; } else { expect(message[0]).to.equal(0xB0); // control change on channel 1 expect(message[1]).to.equal(32); // bankselectfine expect(message[2]).to.equal(34); // bankselectcoarse VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly send MIDI message when using controller names", function(done) { // Arrange let index = 0; let map = [ ["bankselectcoarse", 0], ["modulationwheelcoarse", 1], ["breathcontrollercoarse", 2], ["footcontrollercoarse", 4], ["portamentotimecoarse", 5], ["dataentrycoarse", 6], ["volumecoarse", 7], ["balancecoarse", 8], ["pancoarse", 10], ["expressioncoarse", 11], ["effectcontrol1coarse", 12], ["effectcontrol2coarse", 13], ["generalpurposecontroller1", 16], ["generalpurposecontroller2", 17], ["generalpurposecontroller3", 18], ["generalpurposecontroller4", 19], ["bankselectfine", 32], ["modulationwheelfine", 33], ["breathcontrollerfine", 34], ["footcontrollerfine", 36], ["portamentotimefine", 37], ["dataentryfine", 38], ["channelvolumefine", 39], ["balancefine", 40], ["panfine", 42], ["expressionfine", 43], ["effectcontrol1fine", 44], ["effectcontrol2fine", 45], ["damperpedal", 64], ["portamento", 65], ["sostenuto", 66], ["softpedal", 67], ["legatopedal", 68], ["hold2", 69], ["soundvariation", 70], ["resonance", 71], ["releasetime", 72], ["attacktime", 73], ["brightness", 74], ["decaytime", 75], ["vibratorate", 76], ["vibratodepth", 77], ["vibratodelay", 78], ["controller79", 79], ["generalpurposecontroller5", 80], ["generalpurposecontroller6", 81], ["generalpurposecontroller7", 82], ["generalpurposecontroller8", 83], ["portamentocontrol", 84], ["highresolutionvelocityprefix", 88], ["effect1depth", 91], ["effect2depth", 92], ["effect3depth", 93], ["effect4depth", 94], ["effect5depth", 95], ["dataincrement", 96], ["datadecrement", 97], ["nonregisteredparameterfine", 98], ["nonregisteredparametercoarse", 99], ["registeredparameterfine", 100], ["registeredparametercoarse", 101], ["allsoundoff", 120], ["resetallcontrollers", 121], ["localcontrol", 122], ["allnotesoff", 123], ["omnimodeoff", 124], ["omnimodeon", 125], ["monomodeon", 126], ["polymodeon", 127] ]; VIRTUAL_OUTPUT.on("message", assert); // Act map.forEach(pair => WEBMIDI_OUTPUT.channels[1].sendControlChange(pair[0], 123)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(176); expect(message[1]).to.equal(map[index][1]); index++; if (index >= map.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should properly send MIDI message for deprecated names (legacy)", function(done) { // Arrange let index = 0; let map = [ ["generalpurposeslider1", 16], ["generalpurposeslider2", 17], ["generalpurposeslider3", 18], ["generalpurposeslider4", 19], ["volumefine", 39], ["holdpedal", 64], ["sustenutopedal", 66], ["hold2pedal", 69], ["soundreleasetime", 72], ["soundattacktime", 73], ["soundcontrol6", 75], ["soundcontrol7", 76], ["soundcontrol8", 77], ["soundcontrol9", 78], ["soundcontrol10", 79], ["generalpurposebutton1", 80], ["generalpurposebutton2", 81], ["generalpurposebutton3", 82], ["generalpurposebutton4", 83], ["portamentocontrol", 84], ["highresolutionvelocityprefix", 88], ["reverblevel", 91], ["tremololevel", 92], ["choruslevel", 93], ["celestelevel", 94], ["phaserlevel", 95], ["databuttonincrement", 96], ["databuttondecrement", 97], ["nonregisteredparameterfine", 98], ["nonregisteredparametercoarse", 99], ["registeredparameterfine", 100], ["registeredparametercoarse", 101], ]; VIRTUAL_OUTPUT.on("message", assert); // Act map.forEach(pair => WEBMIDI_OUTPUT.channels[1].sendControlChange(pair[0], 123)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(176); expect(message[1]).to.equal(map[index][1]); index++; if (index >= map.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid controller name is specified", function () { // Arrange let values = [ "xxx" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendControlChange(value, 123); }).to.throw(); } }); it("should throw error when invalid controller number is specified", function () { // Arrange let values = [ -1, 128 ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendControlChange(value, 123); }).to.throw(); } }); it("should throw error when invalid value is specified", function () { // Arrange let invalid = [ NaN, undefined, null ]; // Act invalid.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendControlChange(0, value); }).to.throw(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendControlChange("bankselectcoarse", 0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendNoteOff()", function () { it("should send MIDI message when specifying note by number", function(done) { // Arrange let channel = 1; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 0; i <= 127; i++) WEBMIDI_OUTPUT.channels[channel].sendNoteOff(i, options); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(128); expect(message[1]).to.equal(index); index++; if (index > 127) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send MIDI message when specifying note by name", function(done) { // Arrange let channel = 1; let notes = ["C-1", "C3", "G5", "G9"]; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act notes.forEach(note => WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(128); expect(message[1]).to.equal(Utilities.toNoteNumber(notes[index])); index++; if (index >= notes.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send MIDI message when specifying note by Note object", function(done) { // Arrange let channel = 1; let notes = [new Note("C-1"), new Note("C3"), new Note("G5"), new Note("G9")]; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act notes.forEach(note => WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(128); expect(message[1]).to.equal(notes[index].number); index++; if (index >= notes.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct release velocity when specified with normal value", function(done) { // Arrange let channel = 1; let note = 0; let options = {release: 1}; let expected = [128, 0, 127]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should send correct release velocity when specified with raw value", function(done) { // Arrange let channel = 1; let note = 0; let options = {release: 1, rawRelease: 83}; let expected = [128, 0, options.rawRelease]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should call 'send' method with correct parameters when using 'rawRelease'", function () { // Arrange let channel = 1; let note = 60; let options = {time: 10, release: 0.5, rawRelease: 127}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(note); expect(args[0][2]).to.equal(options.rawRelease); expect(args[1].time).to.equal(options.time); }); it("should call 'send' method with correct parameters when using 'attack'", function () { // Arrange let channel = 1; let note = 60; let options = {time: 10, release: 0.5}; let expectedRawRelease = 64; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note, options); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(note); expect(args[0][2]).to.equal(expectedRawRelease); expect(args[1].time).to.equal(options.time); }); it("should call 'send' with correct 'octaveOffset' when using number", function () { // Arrange const channel = 1; WebMidi.octaveOffset = -1; WEBMIDI_OUTPUT.octaveOffset = -1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = -1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const inputNumber = 60; const outputNumber = inputNumber + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(inputNumber); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should call 'send' with correct 'octaveOffset' when using identifier", function () { // Arrange const channel = 1; WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const note = new Note("C4"); const outputNumber = note.number + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should call 'send' with correct 'octaveOffset' when using Note", function () { // Arrange const channel = 1; WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const note = new Note(60); const outputNumber = note.number + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOff(note); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendNoteOff(0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendNoteOn()", function () { it("should send MIDI message when specifying note by number", function(done) { // Arrange let channel = 1; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 0; i <= 127; i++) { WEBMIDI_OUTPUT.channels[channel].sendNoteOn(i, options); } // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(144); expect(message[1]).to.equal(index); index++; if (index > 127) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send MIDI message when specifying note by name", function(done) { // Arrange let channel = 1; let notes = ["C-1", "C3", "G5", "G9"]; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act notes.forEach(note => WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(144); expect(message[1]).to.equal(Utilities.toNoteNumber(notes[index])); index++; if (index >= notes.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send MIDI message when specifying note by Note object", function(done) { // Arrange let channel = 1; let notes = [new Note("C-1"), new Note("C3"), new Note("G5"), new Note("G9")]; let index = 0; let options = {time: 0}; VIRTUAL_OUTPUT.on("message", assert); // Act notes.forEach(note => WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options)); // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(144); expect(message[1]).to.equal(notes[index].number); index++; if (index >= notes.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct attack velocity when specified with normal value", function(done) { // Arrange let channel = 1; let note = 0; let options = {attack: 1}; let expected = [144, 0, 127]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should send correct attack velocity when specified with raw value", function(done) { // Arrange let channel = 1; let note = 0; let options = {attack: 1, rawAttack: 98}; let expected = [144, 0, options.rawAttack]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should call 'send' method with correct parameters when using 'rawAttack'", function () { // Arrange let channel = 1; let note = 60; let options = {time: 10, attack: 0.5, rawAttack: 127}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(note); expect(args[0][2]).to.equal(options.rawAttack); expect(args[1].time).to.equal(options.time); }); it("should call 'send' method with correct parameters when using 'attack'", function () { // Arrange let channel = 1; let note = 60; let options = {time: 10, attack: 0.5}; let expectedRawAttack = 64; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note, options); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(note); expect(args[0][2]).to.equal(expectedRawAttack); expect(args[1].time).to.equal(options.time); }); it("should call 'send' with correct 'octaveOffset' when using number", function () { // Arrange const channel = 1; WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const inputNumber = 60; const outputNumber = inputNumber + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(inputNumber); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should call 'send' with correct 'octaveOffset' when using identifier", function () { // Arrange const channel = 1; WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const note = new Note("C4"); const outputNumber = note.number + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should call 'send' with correct 'octaveOffset' when using Note", function () { // Arrange const channel = 1; WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[channel].octaveOffset; const note = new Note(60); const outputNumber = note.number + offset * 12; const spy = sinon.spy(WEBMIDI_OUTPUT.channels[channel], "send"); // Act WEBMIDI_OUTPUT.channels[channel].sendNoteOn(note); // Assert let args = spy.args[0]; expect(spy.calledOnce).to.be.true; expect(args[0][1]).to.equal(outputNumber); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[channel].octaveOffset = 0; }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendNoteOn(0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendChannelAftertouch()", function () { it("should send correct MIDI message when using float (0-1)", function(done) { // Arrange let index = 0; let values = [0, 0.5, 1]; let expected = [ [208, 0], [208, 64], [208, 127], ]; VIRTUAL_OUTPUT.on("message", assert); // Act values.forEach(value => WEBMIDI_OUTPUT.channels[1].sendChannelAftertouch(value)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI message when using integer (0-127)", function(done) { // Arrange let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 0; i < 128; i++) { WEBMIDI_OUTPUT.channels[1].sendChannelAftertouch(i, {rawValue: true}); } // Assert function assert(deltaTime, message) { expect(message[1]).to.equal(index++); if (index >= 128) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid pressure is specified", function() { // Arrange let values = [ -1, 2, undefined, null, "test" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendChannelAftertouch(value); }).to.throw(RangeError); } }); it("should throw error when invalid raw pressure is specified", function() { // Arrange let values = [ -1, 128, undefined, null, "test" ]; let options = {rawValue: true}; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendChannelAftertouch(value, options); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendChannelAftertouch(1) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendKeyAftertouch()", function () { it("should send correct MIDI message when using note number", function(done) { // Arrange let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 0; i <= 127; i++) WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(i, 0.5); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members([160, index, 64]); index++; if (index >= 127) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI message when using note identifier", function(done) { // Arrange let index = 0; const value = 0.5; const items = [ {identifier: "C-1", number: 0}, {identifier: "C4", number: 60}, {identifier: "G9", number: 127}, ]; VIRTUAL_OUTPUT.on("message", assert); // Act items.forEach(item => { WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(item.identifier, value); }); // Assert function assert(deltaTime, message) { expect( message ).to.have.ordered.members([160, items[index].number, Utilities.fromFloatTo7Bit(value)]); index++; if (index >= items.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct message when offset is used with note number", function(done) { // Arrange WebMidi.octaveOffset = -1; WEBMIDI_OUTPUT.octaveOffset = -1; WEBMIDI_OUTPUT.channels[1].octaveOffset = -1; const offset = WebMidi.octaveOffset + WEBMIDI_OUTPUT.octaveOffset + WEBMIDI_OUTPUT.channels[1].octaveOffset; let index = Math.abs(offset) * 12; const max = 127; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = index; i <= max; i++) WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(i, 0.5); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members([160, index + offset * 12, 64]); index++; if (index >= max) { VIRTUAL_OUTPUT.removeAllListeners(); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[1].octaveOffset = 0; done(); } } }); it("should send correct message when offset is used with note identifier", function(done) { // Arrange WebMidi.octaveOffset = 1; WEBMIDI_OUTPUT.octaveOffset = 1; WEBMIDI_OUTPUT.channels[1].octaveOffset = 1; let index = 0; const value = 0.5; const items = [ {identifier: "C-1", number: 36}, {identifier: "C4", number: 96} ]; VIRTUAL_OUTPUT.on("message", assert); // Act items.forEach(item => { WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(item.identifier, value); }); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members( [160, items[index].number, Utilities.fromFloatTo7Bit(value)] ); index++; if (index >= items.length) { VIRTUAL_OUTPUT.removeAllListeners(); WebMidi.octaveOffset = 0; WEBMIDI_OUTPUT.octaveOffset = 0; WEBMIDI_OUTPUT.channels[1].octaveOffset = 0; done(); } } }); it("should throw error when invalid pressure is specified", function() { // Arrange let values = [ -1, 2, undefined, null, "test" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(64, value); }).to.throw(RangeError); } }); it("should throw error when invalid raw pressure is specified", function() { // Arrange let values = [ -1, 128, undefined, null, "test" ]; let options = {rawValue: true}; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(64, value, options); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendKeyAftertouch(64, 0.5) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendLocalControl()", function () { it("should call 'sendChannelMode' method with correct parameter for 'true'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendLocalControl(true, options); // Assert expect(spy.calledOnceWithExactly("localcontrol", 127, options)).to.be.true; }); it("should call 'sendChannelMode' method with correct parameter for 'false'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendLocalControl(false, options); // Assert expect(spy.calledOnceWithExactly("localcontrol", 0, options)).to.be.true; }); it("should send correct MIDI message", function(done) { // Arrange let expected = [176, 122, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendLocalControl(); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendLocalControl() ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendMasterTuning()", function () { it("should call 'sendRpnValue' method for coarse and fine tuning", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendRpnValue"); let options = {time: 0}; let value = 12.5; // Act WEBMIDI_OUTPUT.channels[1].sendMasterTuning(value, options); // Assert expect(spy.calledWith("channelcoarsetuning")).to.be.true; expect(spy.calledWith("channelfinetuning")).to.be.true; expect(spy.calledTwice).to.be.true; }); it("should throw error when invalid value is specified", function() { // Arrange let values = [ -65, 64 ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendMasterTuning(value); }).to.throw(RangeError); } }); it("should send correct MIDI messages", function(done) { // Arrange let coarse = 25; let fine = 0.13; let expected = [ // Master coarse tuning 0,2 [ 176, 101, 0 ], [ 176, 100, 2 ], [ 176, 6, coarse + 64 ], [ 176, 101, 127 ], [ 176, 100, 127 ], // Master fine tuning 0,1 [ 176, 101, 0 ], [ 176, 100, 1 ], [ 176, 6, 72 ], // msb [ 176, 38, 40 ], // lsb [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendMasterTuning(coarse + fine); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendMasterTuning(0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendModulationRange()", function () { it("should properly call 'sendRpnValue()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendRpnValue"); let semitones = 8; let cents = 123; let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendModulationRange(semitones, cents, options); // Assert expect( spy.calledOnceWithExactly("modulationrange", [semitones, cents], options) ).to.be.true; }); it("should send correct MIDI messages", function(done) { // Arrange let semitones = 8; let cents = 123; let expected = [ [ 176, 101, 0 ], [ 176, 100, 5 ], [ 176, 6, semitones ], [ 176, 38, cents ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendModulationRange(semitones, cents); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid semitones value is specified", function() { // Arrange let semitones = [ -1, 128, undefined, NaN, null ]; // Act semitones.forEach(assert); // Assert function assert(semitone) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendModulationRange(semitone); }).to.throw(RangeError); } }); it("should throw error if cents value is specified but invalid", function() { // Arrange let values = [ -1, 128, NaN ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendModulationRange(64, value); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendModulationRange(8, 9) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendNrpnValue()", function () { it("should properly call '_selectNonRegisteredParameter()' method", function () { // Arrange let parameter = [8, 123]; let data = 123; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_selectNonRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendNrpnValue(parameter, data, options); // Assert expect( spy.calledOnceWithExactly(parameter, options) ).to.be.true; }); it("should properly call '_setCurrentParameter()' method", function () { // Arrange let parameter = [8, 123]; let data = [47]; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_setCurrentParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendNrpnValue(parameter, data, options); // Assert expect( spy.calledWithExactly(data, options) ).to.be.true; }); it("should properly call '_deselectNonRegisteredParameter()' method", function () { // Arrange let parameter = [8, 123]; let data = 123; let options = {time: 0}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_deselectNonRegisteredParameter"); // Act WEBMIDI_OUTPUT.channels[1].sendNrpnValue(parameter, data, options); // Assert expect( spy.calledOnceWithExactly(options) ).to.be.true; }); it("should send correct MIDI messages when using data array", function(done) { // Arrange let parameter = [8, 123]; let data = [45, 67]; let expected = [ [ 176, 99, parameter[0] ], [ 176, 98, parameter[1] ], [ 176, 6, data[0] ], [ 176, 38, data[1] ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendNrpnValue(parameter, data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI messages when single data byte", function(done) { // Arrange let parameter = [8, 123]; let data = 45; let expected = [ [ 176, 99, parameter[0] ], [ 176, 98, parameter[1] ], [ 176, 6, data ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendNrpnValue(parameter, data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid NRPN value is specified", function() { // Arrange let values = [ 1, [-1, 64], [64, 128], undefined, NaN, null ]; let data = 45; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendNrpnValue(value, data); }).to.throw(); } }); it("should throw error when invalid data is specified", function() { // Arrange let values = [ 1, [-1, 64], [64, 128], undefined, NaN, null ]; let nrpn = 45; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendNrpnValue(nrpn, value); }).to.throw(); } }); it("should return 'OutputChannel' object for method chaining", function () { // Arrange let nrpn = [12, 34]; // Assert expect( WEBMIDI_OUTPUT.channels[1].sendNrpnValue(nrpn, 56) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendOmniMode()", function () { it("should call 'sendChannelMode()' method with correct parameter for 'true'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendOmniMode(true, options); // Assert expect(spy.calledOnceWithExactly("omnimodeon", 0, options)).to.be.true; }); it("should call 'sendChannelMode()' method with correct parameter for 'false'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {}; // Act WEBMIDI_OUTPUT.channels[1].sendOmniMode(false, options); // Assert expect(spy.calledOnceWithExactly("omnimodeoff", 0, options)).to.be.true; }); it("should send correct MIDI message", function(done) { // Arrange let expected = [176, 125, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendOmniMode(); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendOmniMode(true) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendPitchBend()", function () { it("should send correct MIDI message when using float (-1 to 1)", function(done) { // Arrange let index = 0; let values = [-1, 0, 1]; let expected = [ [224, 0, 0], [224, 0, 64], [224, 127, 127] ]; VIRTUAL_OUTPUT.on("message", assert); // Act values.forEach(value => WEBMIDI_OUTPUT.channels[1].sendPitchBend(value)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI message when using integers", function(done) { // Arrange let values = [ [0, 0], [64, 0], [127, 0], [0, 0], [0, 64], [0, 127] ]; let options = {rawValue: true}; let expected = [ [224, 0, 0], [224, 0, 64], [224, 0, 127], [224, 0, 0], [224, 64, 0], [224, 127, 0] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act values.forEach(value => WEBMIDI_OUTPUT.channels[1].sendPitchBend(value, options)); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid value is specified", function() { // Arrange let values = [ undefined, NaN, null, -2, 2 ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendPitchBend(value); }).to.throw(); } }); it("should throw error when invalid raw value is specified", function() { // Arrange let values = [ undefined, NaN, null, -1, 128, [-1, 64], [128, 64], [64, -1], [64, 128], [] ]; let options = {rawValue: true}; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendPitchBend(value, options); }).to.throw(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendPitchBend(0) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendPitchBendRange()", function () { it("should call 'sendRpnValue()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendRpnValue"); let semitones = 8; let cents = 123; let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendPitchBendRange(semitones, cents, options); // Assert expect( spy.calledOnceWithExactly("pitchbendrange", [semitones, cents], options) ).to.be.true; }); it("should send correct MIDI messages", function(done) { // Arrange let semitones = 8; let cents = 123; let expected = [ [ 176, 101, 0 ], [ 176, 100, 0 ], [ 176, 6, semitones ], [ 176, 38, cents ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendPitchBendRange(semitones, cents); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid semitones value is specified", function() { // Arrange let values = [ -1, 128, undefined, NaN, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendPitchBendRange(64, value); }).to.throw(RangeError); } }); it("should throw error when invalid cents value is specified", function() { // Arrange let values = [ -1, 128, undefined, NaN, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendPitchBendRange(64, value); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendPitchBendRange(8, 9) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendPolyphonicMode()", function () { it("should call 'sendChannelMode' method with correct parameter for 'mono'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendPolyphonicMode("mono", options); // Assert expect(spy.calledOnceWithExactly("monomodeon", 0, options)).to.be.true; }); it("should call 'sendChannelMode' method with correct parameter for 'poly'", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {}; // Act WEBMIDI_OUTPUT.channels[1].sendPolyphonicMode("poly", options); // Assert expect(spy.calledOnceWithExactly("polymodeon", 0, options)).to.be.true; }); it("should send correct MIDI message for 'mono'", function(done) { // Arrange let expected = [176, 126, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendPolyphonicMode("mono"); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should send correct MIDI message for 'poly'", function(done) { // Arrange let expected = [176, 127, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendPolyphonicMode("poly"); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendPolyphonicMode("mono") ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendProgramChange()", function () { it("should send correct MIDI message", function(done) { // Arrange let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act for (let i = 0; i <= 127; i++) { WEBMIDI_OUTPUT.channels[1].sendProgramChange(i); } // Assert function assert(deltaTime, message) { expect(message[0]).to.equal(192); expect(message[1]).to.equal(index); index++; if (index > 127) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid value is specified", function() { // Arrange let values = [ -1, 129 ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendProgramChange(value); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendProgramChange(1) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendRpnValue()", function () { it("should properly call '_selectRegisteredParameter()' method", function () { // Arrange let rpn = [0x3D, 0x00]; let data = 123; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_selectRegisteredParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data, options); // Assert expect( spy.calledOnceWithExactly(rpn, options) ).to.be.true; }); it("should properly call '_setCurrentParameter()' method", function () { // Arrange let rpn = [0x00, 0x00]; let data = 47; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_setCurrentParameter"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data, options); // Assert expect( spy.calledWithExactly(data, options) ).to.be.true; }); it("should properly call '_deselectRegisteredParameter()' method", function () { // Arrange let rpn = [0x00, 0x01]; let data = 123; let options = {time: 0}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "_deselectRegisteredParameter"); // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data, options); // Assert expect( spy.calledOnceWithExactly(options) ).to.be.true; }); it("should send correct MIDI messages when using RPN name", function(done) { // Arrange let rpn = "modulationrange"; let rpnAsNumbers = [0x00, 0x05]; let data = [45, 67]; let expected = [ [ 176, 101, rpnAsNumbers[0] ], [ 176, 100, rpnAsNumbers[1] ], [ 176, 6, data[0] ], [ 176, 38, data[1] ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI messages when using data array", function(done) { // Arrange let rpn = [0x00, 0x02]; let data = [45, 67]; let expected = [ [ 176, 101, rpn[0] ], [ 176, 100, rpn[1] ], [ 176, 6, data[0] ], [ 176, 38, data[1] ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should send correct MIDI messages when using single data byte", function(done) { // Arrange let rpn = [8, 123]; let data = 45; let expected = [ [ 176, 101, rpn[0] ], [ 176, 100, rpn[1] ], [ 176, 6, data ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, data); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid RPN value is specified", function() { // Arrange let values = [ 1, [-1, 64], [64, 128], undefined, NaN, null ]; let data = 45; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnValue(value, data); }).to.throw(); } }); it("should throw error when invalid data is specified", function() { // Arrange let values = [ 1, [-1, 64], [64, 128], undefined, NaN, null ]; let rpn = 45; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendRpnValue(rpn, value); }).to.throw(); } }); it("should return 'OutputChannel' object for method chaining", function () { // Arrange let nrpn = [12, 34]; // Assert expect( WEBMIDI_OUTPUT.channels[1].sendRpnValue(nrpn, 56) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendTuningBank()", function () { it("should call 'sendRpnValue()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendRpnValue"); let value = 8; let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendTuningBank(value, options); // Assert expect( spy.calledOnceWith("tuningbank", value, options) ).to.be.true; }); it("should send correct MIDI messages", function(done) { // Arrange let value = 8; let expected = [ [ 176, 101, 0 ], [ 176, 100, 4 ], [ 176, 6, value ], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendTuningBank(value); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid value is specified", function() { // Arrange let values = [ -1, 129, undefined, NaN, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendTuningBank(value); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendTuningBank(8) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendTuningProgram()", function () { it("should call 'sendRpnValue()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendRpnValue"); let value = 8; let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendTuningProgram(value, options); // Assert expect( spy.calledOnceWith("tuningprogram", value, options) ).to.be.true; }); it("should send correct MIDI messages", function(done) { // Arrange let value = 8; let expected = [ [ 176, 101, 0 ], [ 176, 100, 3 ], [ 176, 6, value], [ 176, 101, 127 ], [ 176, 100, 127 ] ]; let index = 0; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendTuningProgram(value); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected[index]); index++; if (index >= expected.length) { VIRTUAL_OUTPUT.removeAllListeners(); done(); } } }); it("should throw error when invalid value is specified", function() { // Arrange let values = [ -1, 129, undefined, NaN, null ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { WEBMIDI_OUTPUT.channels[1].sendTuningProgram(value); }).to.throw(RangeError); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendTuningProgram(8) ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("stopNote()", function () { it("should properly call 'sendNoteOff()' method", function () { // Arrange let channel = 1; let note = "G5"; let options = {time: 0}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // Act WEBMIDI_OUTPUT.channels[channel].stopNote(note, options); // Assert expect( spy.calledOnceWithExactly(note, options) ).to.be.true; }); it("should call the 'sendNoteOff()' method with correct parameters", function () { // Arrange let note = "G5"; let options = {time: 10, release: 0.5, rawRelease: 127}; let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendNoteOff"); // Act WEBMIDI_OUTPUT.channels[1].stopNote(note, options); // Assert expect(spy.calledOnceWith(note)).to.be.true; expect(spy.args[0][0]).to.equal(note); expect(spy.args[0][1].time).to.equal(options.time); expect(spy.args[0][1].release).to.equal(options.release); expect(spy.args[0][1].rawRelease).to.equal(options.rawRelease); }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].playNote("C3") ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendAllNotesOff()", function () { it("should properly call 'sendChannelMode()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendAllNotesOff(options); // Assert expect(spy.calledOnceWithExactly("allnotesoff", 0, options)).to.be.true; }); it("should send correct MIDI message", function(done) { // Arrange let expected = [176, 123, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendAllNotesOff(); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendAllNotesOff() ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); describe("sendAllSoundOff()", function () { it("should properly call 'sendChannelMode()' method", function () { // Arrange let spy = sinon.spy(WEBMIDI_OUTPUT.channels[1], "sendChannelMode"); let options = {time: 0}; // Act WEBMIDI_OUTPUT.channels[1].sendAllSoundOff(options); // Assert expect(spy.calledOnceWithExactly("allsoundoff", 0, options)).to.be.true; }); it("should send the correct MIDI message", function(done) { // Arrange let expected = [176, 120, 0]; VIRTUAL_OUTPUT.on("message", assert); // Act WEBMIDI_OUTPUT.channels[1].sendAllSoundOff(); // Assert function assert(deltaTime, message) { expect(message).to.have.ordered.members(expected); VIRTUAL_OUTPUT.removeAllListeners(); done(); } }); it("should return 'OutputChannel' object for method chaining", function () { expect( WEBMIDI_OUTPUT.channels[1].sendAllSoundOff() ).to.equal(WEBMIDI_OUTPUT.channels[1]); }); }); }); ================================================ FILE: test/Utilities.test.js ================================================ const expect = require("chai").expect; const {Utilities, WebMidi, Note, Enumerations} = require("../dist/cjs/webmidi.cjs.js"); // VERIFIED describe("Utilities Object", function() { describe("toNoteNumber()", function() { it("should return the correct MIDI note number", function() { // Arrange const pairs = [ {identifier: "C-1", number: 0}, {identifier: "C4", number: 60}, {identifier: "G9", number: 127} ]; // Act pairs.forEach(assert); // Assert function assert(pair) { expect(Utilities.toNoteNumber(pair.identifier)).to.equal(pair.number); } }); it("should throw error if invalid identifier is provided", function() { // Arrange const values = [ "", "abc", null, undefined, "G#9", "Cb-1", "X2", function () {}, {}, "555", "C-3", "Cbb-1", "H3", "G##9" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { Utilities.toNoteNumber(value); }).to.throw(); } }); it("should return the correct MIDI note number when using octaveOffset", function() { // Arrange const items = [ {identifier: "C-1", octaveOffset: 1, number: 12}, {identifier: "C4", octaveOffset: -1, number: 48}, {identifier: "C4", octaveOffset: 1, number: 72}, {identifier: "G9", octaveOffset: -1, number: 115}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.toNoteNumber(item.identifier, item.octaveOffset) ).to.equal(item.number); } }); it("should throw when using invalid octaveOffset", function() { // Arrange const items = [ NaN, Infinity, -Infinity, [], {}, -1, 20 ]; // Act items.forEach(assert); // Assert function assert(item) { expect(function () { Utilities.toNoteNumber("C-1", item); }).to.throw(RangeError); } }); }); describe("getNoteDetails()", function() { it("should return the correct fragments when using identifier", function () { // Arrange const items = [ {identifier: "C-1", name: "C", accidental: undefined, octave: -1}, {identifier: "D##0", name: "D", accidental: "##", octave: 0}, {identifier: "E#1", name: "E", accidental: "#", octave: 1}, {identifier: "Fb8", name: "F", accidental: "b", octave: 8}, {identifier: "Fbb9", name: "F", accidental: "bb", octave: 9}, {identifier: "G9", name: "G", accidental: undefined, octave: 9}, ]; // Act items.forEach(assert); // Assert function assert(item) { const fragments = Utilities.getNoteDetails(item.identifier); expect(fragments.identifier).to.equal(item.identifier); expect(fragments.name).to.equal(item.name); expect(fragments.accidental).to.equal(item.accidental); expect(fragments.octave).to.equal(item.octave); } }); it("should return the correct fragments when using number", function () { // Arrange const items = [ {number: 0, name: "C", accidental: undefined, octave: -1}, {number: 59, name: "B", accidental: undefined, octave: 3}, {number: 60, name: "C", accidental: undefined, octave: 4}, {number: 61, name: "C", accidental: "#", octave: 4}, {number: 127, name: "G", accidental: undefined, octave: 9}, ]; // Act items.forEach(assert); // Assert function assert(item) { const fragments = Utilities.getNoteDetails(item.number); expect(fragments.name).to.equal(item.name); expect(fragments.accidental).to.equal(item.accidental); expect(fragments.octave).to.equal(item.octave); } }); it("should should throw when passing invalid note identifier", function () { // Arrange const items = [ NaN, Infinity, -Infinity, [], {}, -1, 20, "C###3", "Bbbb6", "test" ]; // Act items.forEach(assert); // Assert function assert(item) { expect(function () { Utilities.toNoteNumber(item); }).to.throw(TypeError); } }); }); describe("getCcNameByNumber()", function() { it("should return the correct name", function () { // Arrange const items = [-1, 128, null, undefined, "test"]; // Act // Assert for (let i = 0; i <= 127; i++) { expect(Utilities.getCcNameByNumber(i)) .to.equal(Enumerations.CONTROL_CHANGE_MESSAGES[i].name); } items.forEach(item => { expect(Utilities.getCcNameByNumber(item)).to.equal(undefined); }); }); }); describe("getCcNumberByName()", function() { it("should return the correct number", function () { // Arrange const items = [ {name: "bankselectcoarse", number: 0}, {name: "controller31", number: 31}, {name: "attacktime", number: 73}, {name: "polymodeon", number: 127}, ]; // Act // Assert items.forEach(item => { expect(Utilities.getCcNumberByName(item.name)).to.equal(item.number); }); }); it("should return undefined for invalid names", function () { // Arrange const items = ["", undefined, null, "test"]; // Act // Assert items.forEach(item => { expect(Utilities.getCcNumberByName(item)).to.equal(undefined); }); }); }); describe("sanitizeChannels()", function() { it("should return only valid MIDI channel numbers", function() { // Valid values are 1, 8 and 16 let channels = [ 1, -1, -2.3, 0, 17, "x", NaN, null, Infinity, 8, -Infinity, {}, true, false, [undefined], 4e2, 16 ]; expect(Utilities.sanitizeChannels(channels).length).to.equal(3); expect(Utilities.sanitizeChannels([]).length).to.equal(0); expect(Utilities.sanitizeChannels([9.7])[0]).to.equal(9); expect(Utilities.sanitizeChannels([1e1])[0]).to.equal(10); expect(Utilities.sanitizeChannels("none").length).to.equal(0); }); it('should return all channels when "all" is specified', function() { expect(Utilities.sanitizeChannels("all").length).to.equal(16); }); it("should return an empty array when 'none' is passed", function() { expect(Utilities.sanitizeChannels("none").length).to.equal(0); }); }); describe("convertToTimestamp()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should return timestamp when passed string starting with '+'", function() { // Assert expect(Utilities.toTimestamp("+1000")).to.be.a("number"); }); it("should return false for invalid input", function() { // Arrange let values = [undefined, null, false, [], {}, "", "-1", "+", -1, -Infinity]; // Act values.forEach(assert); // Assert function assert(value) { expect(Utilities.toTimestamp(value)).to.be.false; } }); it("should return a positive number as is", function() { // Arrange let values = [0, 1, Infinity]; // Act values.forEach(assert); // Assert function assert(value) { expect(Utilities.toTimestamp(value)).to.equal(value); } }); }); describe("toNoteIdentifier()", function() { it("should return the correct note identifier", function() { // Arrange const items = [ {number: 0, identifier: "C-1"}, {number: 60, identifier: "C4"}, {number: 127, identifier: "G9"}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect(Utilities.toNoteIdentifier(item.number)).to.equal(item.identifier); } }); it("should return the correct identifier when using 'octaveOffset'", function() { // Arrange const items = [ {identifier: "C-1", octaveOffset: 1, number: 12}, {identifier: "C4", octaveOffset: -1, number: 48}, {identifier: "C4", octaveOffset: 1, number: 72}, {identifier: "G9", octaveOffset: -1, number: 115}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.toNoteNumber(item.identifier, item.octaveOffset) ).to.equal(item.number); } }); it("should throw error if invalid number is provided", function() { // Arrange const values = [ -1, 128, "", "abc", null, undefined, {}, "555" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(function() { Utilities.toNoteIdentifier(value); }).to.throw(); } }); it("should throw when using invalid octaveOffset", function() { // Arrange const items = [ NaN, Infinity, -Infinity, [], {}, "abc" ]; // Act items.forEach(assert); // Assert function assert(item) { expect(function () { Utilities.toNoteIdentifier(60, item); }).to.throw(RangeError); } }); }); describe("guessNoteNumber()", function() { it("should return the correct number for ints and floats", function() { // Arrange const items = [ 0, 0.0, 0.1, 0.5, 0.9 ]; // Act for (let i = 0; i <= 127; i++) { assert(i); } // Assert function assert(value) { items.forEach(item => { expect( Utilities.guessNoteNumber(value + item) ).to.equal(value); }); } }); it("should return the correct number for numbers in strings", function() { // Arrange const items = [ "abc", ".", undefined, null ]; // Act for (let i = 0; i <= 127; i++) { assert(i); } // Assert function assert(value) { items.forEach(item => { expect( Utilities.guessNoteNumber(value.toString() + item) ).to.equal(value); }); } }); it("should return the correct number for note identifiers", function() { // Arrange const items = [ {identifier: "C-1", number: 0}, {identifier: "D#0", number: 15}, {identifier: "D##0", number: 16}, {identifier: "C4", number: 60}, {identifier: "Eb4", number: 63}, {identifier: "Ebb4", number: 62}, {identifier: "G9", number: 127}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.guessNoteNumber(item.identifier) ).to.equal(item.number); } }); it("should strip whitespace for note identifiers", function() { // Arrange const items = [ {identifier: " C-1 ", number: 0}, {identifier: "\nD#0", number: 15}, {identifier: "D##0\t", number: 16}, {identifier: "\rC4", number: 60}, {identifier: "\rEb4\r", number: 63}, {identifier: "\n\r\t Ebb4", number: 62}, {identifier: "G9\n\r\t ", number: 127}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.guessNoteNumber(item.identifier) ).to.equal(item.number); } }); it("should return the correct number for note identifiers when using octaveOffset", function() { // Arrange const items = [ {identifier: "C-1", number: 0, offset: 0}, {identifier: "C-1", number: 12, offset: 1}, {identifier: "C4", number: 60, offset: 0}, {identifier: "C4", number: 48, offset: -1}, {identifier: "C4", number: 72, offset: 1}, {identifier: "G9", number: 127, offset: 0}, {identifier: "G9", number: 115, offset: -1}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.guessNoteNumber(item.identifier, item.offset) ).to.equal(item.number); } }); it("should disregard octaveOffset for note numbers", function() { // Arrange const items = [ {number: 0, offset: -3}, {number: 64, offset: -2}, {number: 127, offset: -1}, {number: "0", offset: 1}, {number: "64.0", offset: 2}, {number: "127x", offset: 3}, {number: 0.1, offset: 4}, {number: 64.5, offset: 5}, {number: 127.9, offset: 6}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.guessNoteNumber(item.number, item.offset) ).to.equal(parseInt(item.number)); } }); it("should return false for identifiers when using out-of-bounds octaveOffset", function() { // Arrange const items = [ {identifier: "C-1", offset: -10}, {identifier: "C-1", offset: -1}, {identifier: "G9", offset: 1}, {identifier: "G9", offset: 10}, {identifier: "C4", offset: 100}, {identifier: "C4", offset: -100} ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.guessNoteNumber(item.identifier, item.offset) ).to.be.false; } }); it("should return false if invalid input is provided", function() { // Arrange const items = [ "abc", null, undefined, -1, -1.2, 128, 128.1, function () {}, {}, "555", "H3", "Z#8", Infinity, -Infinity, -2.3, false, true ]; // Act items.forEach(assert); // Assert function assert(item) { expect(Utilities.guessNoteNumber(item)).to.be.false; }; }); }); describe("offsetNumber()", function () { it("should use 0 if octaveOffset is invalid", function() { // Arrange let number = 60; let values = [ Infinity, -Infinity, "abc", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(Utilities.offsetNumber(number, value)).to.equal(number); } }); it("should use 0 if semitoneOffset is invalid", function() { // Arrange let number = 60; let values = [ Infinity, -Infinity, "abc", NaN, null, undefined ]; // Act values.forEach(assert); // Assert function assert(value) { expect(Utilities.offsetNumber(number, 0, value)).to.equal(number); } }); it("should cap returned value at 127", function() { // Arrange let number = 60; let pairs = [ {octaveOffset: 0, semitoneOffset: 1000}, {octaveOffset: 100, semitoneOffset: 0}, {octaveOffset: 20, semitoneOffset: 20}, ]; // Act pairs.forEach(assert); // Assert function assert(pair) { expect( Utilities.offsetNumber(number, pair.octaveOffset, pair.semitoneOffset) ).to.equal(127); } }); it("should return 0 for values smaller than 0", function() { // Arrange let number = 60; let pairs = [ {octaveOffset: 0, semitoneOffset: -200}, {octaveOffset: -20, semitoneOffset: 0}, {octaveOffset: -20, semitoneOffset: -20}, ]; // Act pairs.forEach(assert); // Assert function assert(pair) { expect( Utilities.offsetNumber(number, pair.octaveOffset, pair.semitoneOffset) ).to.equal(0); } }); }); describe("buildNote()", function() { it("should return the same note if a note is passed in", function () { // Arrange const note = new Note("C4", {attack: 0.12, release: 0.34, duration: 56}); // Act const result = Utilities.buildNote(note); // Assert expect(result).to.equal(note); expect(result.identifier).to.equal(note.identifier); expect(result.attack).to.equal(note.attack); expect(result.release).to.equal(note.release); expect(result.duration).to.equal(note.duration); }); it("should return the right note when passing identifier", function () { // Arrange const identifier = "C4"; const options = {attack: 0.12, release: 0.34, duration: 56}; // Act const result = Utilities.buildNote(identifier, options); // Assert expect(result.identifier).to.equal(identifier); expect(result.attack).to.equal(options.attack); expect(result.release).to.equal(options.release); expect(result.duration).to.equal(options.duration); }); it("should return the right note when passing identifier and octaveOffset", function () { // Arrange const items = [ {target: "C0", offset: 1, identifier: "C-1"}, {target: "C2", offset: -2, identifier: "C4"}, {target: "C6", offset: 2, identifier: "C4"}, {target: "G8", offset: -1, identifier: "G9"} ]; // Act items.forEach(assert); // Assert function assert(item) { const n = Utilities.buildNote(item.identifier, {octaveOffset: item.offset}); expect(n.identifier).to.equal(item.target); } }); it("should return the right note when passing number", function () { // Arrange const number = 60; const identifier = "C4"; const options = {attack: 0.98, release: 0.76, duration: 56}; // Act const result = Utilities.buildNote(number, options); // Assert expect(result.identifier).to.equal(identifier); expect(result.attack).to.equal(options.attack); expect(result.release).to.equal(options.release); expect(result.duration).to.equal(options.duration); }); it("should disregard octaveOffset when passing number", function () { // Arrange const items = [ {number: 0, offset: -3, identifier: "C-1"}, {number: 64, offset: -2, identifier: "E4"}, {number: 127, offset: -1, identifier: "G9"}, {number: "0", offset: 1, identifier: "C-1"}, {number: "64.0", offset: 2, identifier: "E4"}, {number: "127x", offset: 3, identifier: "G9"}, {number: 0.1, offset: 4, identifier: "C-1"}, {number: 64.5, offset: 5, identifier: "E4"}, {number: 127.9, offset: -6, identifier: "G9"} ]; // Act items.forEach(assert); // Assert function assert(item) { const n = Utilities.buildNote(item.number, {octaveOffset: item.offset}); expect(n.identifier).to.equal(item.identifier); } }); it("should throw if invalid input is provided", function() { // Arrange const items = [ "abc", null, undefined, -1, -1.2, 128, 128.1, function () {}, {}, "555", "H3", "Z#8", Infinity, -Infinity, -2.3, false, true ]; // Act items.forEach(assert); // Assert function assert(item) { expect(function () { Utilities.buildNote(item); }).to.throw(TypeError); }; }); }); describe("buildNoteArray()", function() { it("should return an array of valid notes when passing number array", function () { // Arrange const items = [ 0, 60, 127 ]; const targets = [ "C-1", "C4", "G9" ]; // Act const results = Utilities.buildNoteArray(items); // Assert results.forEach((result, index) => { expect(result.identifier).to.equal(targets[index]); }); }); it("should return an array of valid notes when passing identifier array", function () { // Arrange const items = [ "C-1", "C4", "G9" ]; // Act const results = Utilities.buildNoteArray(items); // Assert results.forEach((result, index) => { expect(result.identifier).to.equal(items[index]); }); }); it("should return an array of valid notes when passing notes array", function () { // Arrange const items = [ new Note("C-1"), new Note("C4"), new Note("G9") ]; // Act const results = Utilities.buildNoteArray(items); // Assert results.forEach((result, index) => { expect(result).to.equal(items[index]); }); }); it("should return an array of valid notes when passing single number", function () { // Arrange const items = [ 0, 60, 127 ]; const targets = [ "C-1", "C4", "G9" ]; const results = []; // Act items.forEach(item => { results.push(Utilities.buildNoteArray(item)); }); // Assert results.forEach((result, index) => { expect(result[0].identifier).to.equal(targets[index]); }); }); it("should return an array of valid notes when passing single identifier", function () { // Arrange const items = [ "C-1", "C4", "G9" ]; const results = []; // Act items.forEach(item => { results.push(Utilities.buildNoteArray(item)); }); // Assert results.forEach((result, index) => { expect(result[0].identifier).to.equal(items[index]); }); }); it("should return an array of valid notes when passing single note", function () { // Arrange const items = [ new Note("C-1"), new Note("C4"), new Note("G9") ]; const results = []; // Act items.forEach(item => { results.push(Utilities.buildNoteArray(item)); }); // Assert results.forEach((result, index) => { expect(result[0].identifier).to.equal(items[index].identifier); }); }); it("should throw error for invalid values", function() { // Arrange let values = [ "abc", null, undefined, -1, 128, {}, "555", "H3", "Z#8", Infinity, -Infinity, "G10", "C-2" ]; // Act values.forEach(assert); // Assert function assert(value) { expect(() => { Utilities.buildNoteArray(value); }).to.throw(); } }); it("should pass on options to values that aren't 'Note' objects", function() { let options = { duration: 1000, attack: 0.56, release: 0.78 }; let notes1 = Utilities.buildNoteArray([60], options); expect(notes1[0].duration).to.equal(1000); expect(notes1[0].attack).to.equal(options.attack); expect(notes1[0].release).to.equal(options.release); }); }); describe("from7bitToFloat()", function() { it("should return the correct value for normal input", function () { // Arrange const items = [ {input: 0, output: 0}, {input: 64, output: 64/127}, {input: 127, output: 1}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.from7bitToFloat(item.input) ).to.equal(item.output); } }); it("should return the correct value for out of bounds input", function () { // Arrange const items = [ {input: -1, output: 0}, {input: -10, output: 0}, {input: 128, output: 1}, {input: 1280, output: 1}, {input: Infinity, output: 1}, {input: -Infinity, output: 0}, {input: 127.1, output: 1}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.from7bitToFloat(item.input) ).to.equal(item.output); } }); }); describe("fromFloatTo7Bit()", function() { it("should return the correct value for normal input", function () { // Arrange const items = [ {input: 0, output: 0}, {input: 0.5, output: 64}, {input: 1, output: 127}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.fromFloatTo7Bit(item.input) ).to.equal(item.output); } }); it("should return the correct value for out of bounds input", function () { // Arrange const items = [ {input: -1, output: 0}, {input: -10, output: 0}, {input: 2, output: 127}, {input: 20, output: 127}, {input: Infinity, output: 127}, {input: -Infinity, output: 0}, {input: 1.1, output: 127}, ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.fromFloatTo7Bit(item.input) ).to.equal(item.output); } }); }); describe("fromMsbLsbToFloat()", function() { it("should return the correct value for normal input", function () { // Arrange const items = [ {msb: 0, lsb: 0, output: 0/16383}, {msb: 0, lsb: 64, output: 64/16383}, {msb: 0, lsb: 127, output: 127/16383}, {msb: 64, lsb: 0, output: 8192/16383}, {msb: 64, lsb: 64, output: 8256/16383}, {msb: 64, lsb: 127, output: 8319/16383}, {msb: 127, lsb: 0, output: 16256/16383}, {msb: 127, lsb: 64, output: 16320/16383}, {msb: 127, lsb: 127, output: 16383/16383} ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.fromMsbLsbToFloat(item.msb, item.lsb) ).to.equal(item.output); } }); it("should return the correct value for out of bounds input", function () { // Arrange const items = [ {msb: -1, lsb: 0, output: 0}, {msb: 0, lsb: -1, output: 0}, {msb: 128, lsb: 0, output: 16256/16383}, {msb: 0, lsb: 128, output: 127/16383} ]; // Act items.forEach(assert); // Assert function assert(item) { expect( Utilities.fromMsbLsbToFloat(item.msb, item.lsb) ).to.equal(item.output); } }); }); describe("fromFloatToMsbLsb()", function() { it("should return the correct value for normal input", function () { // Arrange const items = [ {input: 0, msb: 0, lsb: 0}, {input: 0.25, msb: 32, lsb: 0}, {input: 0.5, msb: 64, lsb: 0}, {input: 0.75, msb: 95, lsb: 127}, {input: 1, msb: 127, lsb: 127} ]; // Act items.forEach(assert); // Assert function assert(item) { let {msb, lsb} = Utilities.fromFloatToMsbLsb(item.input); expect(msb).to.equal(item.msb); expect(lsb).to.equal(item.lsb); } }); it("should return the correct value for out of bounds input", function () { // Arrange const items = [ {input: -10, msb: 0, lsb: 0}, {input: -1, msb: 0, lsb: 0}, {input: 1.2, msb: 127, lsb: 127}, {input: 12, msb: 127, lsb: 127}, ]; // Act items.forEach(assert); // Assert function assert(item) { let {msb, lsb} = Utilities.fromFloatToMsbLsb(item.input); expect(msb).to.equal(item.msb); expect(lsb).to.equal(item.lsb); } }); }); describe("isNode", function() { it("should return the correct value depending on environment", function () { // Assert expect(Utilities.isNode).to.be.true; }); }); describe("isBrowser", function() { it("should return the correct value depending on environment", function () { // Assert expect(Utilities.isBrowser).to.be.false; }); }); describe("getPropertyByValue()", function() { it("should return the correct property"); }); }); ================================================ FILE: test/WebMidi.test.js ================================================ const expect = require("chai").expect; const {WebMidi, Utilities} = require("../dist/cjs/webmidi.cjs.js"); const midi = require("@julusian/midi"); const semver = require("semver"); // The virtual port from the 'midi' library is an "external" device so an output is seen as an input // by WebMidi. To avoid confusion, the naming scheme adopts WebMidi's perspective. const VIRTUAL_INPUT_NAME = "Virtual Input"; const VIRTUAL_INPUT = new midi.Output(VIRTUAL_INPUT_NAME); const VIRTUAL_OUTPUT_NAME = "Virtual Output"; const VIRTUAL_OUTPUT = new midi.Input(VIRTUAL_OUTPUT_NAME); describe("WebMidi Object", function() { afterEach("Disable WebMidi.js", async function () { await WebMidi.disable(); }); it("should not allow itself to be instantiated", function() { // Assert expect(function() { new WebMidi(); }).to.throw(TypeError); }); it("should not allow itself to be instantiated through 'constructor' property", function() { // Assert expect(function() { new WebMidi.constructor(); }).to.throw(TypeError); }); it("should remain extensible", function() { // Assert expect(Object.isExtensible(WebMidi)).to.be.true; }); it("should have empty 'inputs' and 'outputs' arrays", function() { // Assert expect(WebMidi.inputs.length).to.equal(0); expect(WebMidi.outputs.length).to.equal(0); }); it("should report valid version", function() { expect(semver.valid(WebMidi.version)).to.not.be.null; }); it("should report valid flavour", function() { expect(WebMidi.flavour).to.equal("cjs"); }); // THIS WORKS BY ITSELF BUT STOPS WORKING WHEN THE OTHER TESTS ARE RUN! it("should trigger 'connected' events for new inputs"); // it("should trigger 'connected' events for new inputs", function(done) { // // WebMidi.addListener("connected", e => { // if (e.port.name === VIRTUAL_INPUT_NAME) { // WebMidi.removeListener(); // VIRTUAL_INPUT.closePort(); // done(); // } // }); // // // Assert // WebMidi.enable().then(() => { // setTimeout(function () { // VIRTUAL_INPUT.openVirtualPort(VIRTUAL_INPUT_NAME); // }, 100); // }); // // }); // THIS WORKS BY ITSELF BUT STOPS WORKING WHEN THE OTHER TESTS ARE RUN! it("should trigger 'connected' events for new outputs"); // it("should trigger 'connected' events for new outputs", function(done) { // // WebMidi.addListener("connected", e => { // if (e.port.name === VIRTUAL_OUTPUT_NAME) { // WebMidi.removeListener(); // VIRTUAL_OUTPUT.closePort(); // done(); // } // }); // // // Assert // WebMidi.enable().then(() => { // setTimeout(()=>{ // VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); // }, 250); // }); // // }); it("should trigger 'disconnected' events for disconnected outputs"); // it("should trigger 'disconnected' events for disconnected outputs", function(done) { // // VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); // // WebMidi.addListener("disconnected", e => { // if (e.port.name === VIRTUAL_OUTPUT_NAME) { // WebMidi.removeListener(); // done(); // } // }); // // // Assert // WebMidi.enable().then(() => { // setTimeout(() => { // VIRTUAL_OUTPUT.closePort(); // }, 250); // }); // // }); it("should trigger 'disconnected' events for disconnected inputs"); // it("should trigger 'disconnected' events for disconnected inputs", function(done) { // // VIRTUAL_INPUT.openVirtualPort(VIRTUAL_INPUT_NAME); // // WebMidi.addListener("disconnected", e => { // if (e.port.name === VIRTUAL_INPUT_NAME) { // WebMidi.removeListener(); // done(); // } // }); // // // Assert // WebMidi.enable().then(() => { // setTimeout(() => { // VIRTUAL_INPUT.closePort(); // }, 250); // }); // // }); // VERIFIED describe("constructor()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should adjust to Node.js environment", function() { // Assert expect(typeof navigator.requestMIDIAccess).to.equal("function"); expect(typeof performance.now).to.equal("function"); }); }); // VERIFIED describe("disable()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should set 'enabled' property to false", function(done) { // Assert WebMidi.disable().then(() => { expect(WebMidi.enabled).to.equal(false); done(); }); }); it("should set the 'sysexEnabled' property to false", function(done) { // Assert WebMidi.disable().then(() => { expect(WebMidi.sysexEnabled).to.equal(false); done(); }); }); it("should trim 'input' and 'output' array lengths to 0", function(done) { // Assert WebMidi.disable().then(() => { expect(WebMidi.inputs.length).to.equal(0); expect(WebMidi.outputs.length).to.equal(0); done(); }); }); it("should remove all user handlers", async function() { // Arrange WebMidi.addListener("connected", () => {}); WebMidi.addListener("disconnected", () => {}); WebMidi.addListener("enabled", () => {}); WebMidi.addListener("disabled", () => {}); // Act await WebMidi.disable(); // Assert expect(WebMidi.hasListener()).to.be.false; }); it("should set 'interface' to 'null'", async function() { // Act await WebMidi.disable(); // Assert expect(WebMidi.interface).to.be.null; }); it("should dispatch 'disabled' event", function (done) { WebMidi.addListener("disabled", () => done()); WebMidi.disable(); }); }); // VERIFIED describe("enable()", function() { it("should return a resolved promise if already enabled", function(done) { WebMidi.enable().then(() => { WebMidi.enable().then(() => { done(); }); }); }); it("should execute the callback if already enabled (legacy)", function(done) { WebMidi.enable(function() { WebMidi.enable(function() { done(); }); }); }); it("should pass error to callback upon failure to create interface (legacy)", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } // Assert WebMidi.enable(function (err) { expect(err).to.be.an("error"); navigator.requestMIDIAccess = backup; done(); }); }); it("should pass error to callback upon failure to create interface", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } // Assert WebMidi.enable({callback: err => { expect(err).to.be.an("error"); navigator.requestMIDIAccess = backup; done(); }}).catch(() => {}); }); it("should trigger error event upon failure to create interface", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } // Assert WebMidi.addListener("error", e => { expect(e.error).to.be.an("error"); navigator.requestMIDIAccess = backup; done(); }); // Act WebMidi.enable(); }); it("should return a rejected promise upon failure to create interface", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } // Assert WebMidi.enable().catch(err => { navigator.requestMIDIAccess = backup; expect(err).to.be.an("error"); done(); }); }); it("should set 'enabled' property to false if enable() method fails", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } // Assert WebMidi.enable().catch(() => { expect(WebMidi.enabled).to.equal(false); navigator.requestMIDIAccess = backup; done(); }); }); it("should set 'enabled' property to false if enable() method fails (legacy)", function(done) { // Arrange let backup; if (navigator && navigator.requestMIDIAccess) { backup = navigator.requestMIDIAccess; navigator.requestMIDIAccess = () => Promise.reject(new Error("Simulated failure!")); } WebMidi.enable(function () { expect(WebMidi.enabled).to.equal(false); navigator.requestMIDIAccess = backup; done(); }); }); it("should set 'enabled' property to true if successful", function(done) { WebMidi.enable({callback: function (err) { if (err) { // This could happen if WebMIDIAPIShim is there but not the Jazz-Plugin expect(WebMidi.enabled).to.equal(false); } else { expect(WebMidi.enabled).to.equal(true); } done(); }}); }); it("should set 'enabled' property to true if successful (legacy)", function(done) { WebMidi.enable(function (err) { if (err) { // This could happen if WebMIDIAPIShim is there but not the Jazz-Plugin expect(WebMidi.enabled).to.equal(false); } else { expect(WebMidi.enabled).to.equal(true); } done(); }); }); it("should execute the callback if successful", function(done) { WebMidi.enable({callback: function () { done(); }}); }); it("should execute the callback if successful (legacy)", function(done) { WebMidi.enable(function() { done(); }); }); it("should return a promise fulfilled with the WebMidi objectif successful", function(done) { WebMidi.enable().then(wm => { expect(wm).to.equal(WebMidi); done(); }); }); it("should trigger 'connected' events for already connected inputs", function(done) { // Arrange VIRTUAL_INPUT.openVirtualPort(VIRTUAL_INPUT_NAME); WebMidi.addListener("connected", e => { if (e.port.name === VIRTUAL_INPUT_NAME) { WebMidi.removeListener(); VIRTUAL_INPUT.closePort(); done(); } }); // Assert WebMidi.enable(); }); it("should trigger 'connected' events for already connected outputs", function(done) { VIRTUAL_OUTPUT.openVirtualPort(VIRTUAL_OUTPUT_NAME); WebMidi.addListener("connected", e => { if (e.port.name === VIRTUAL_OUTPUT_NAME) { WebMidi.removeListener(); VIRTUAL_OUTPUT.closePort(); done(); } }); // Assert WebMidi.enable(); }); it("should enable sysex only if requested", function(done) { WebMidi.enable({sysex: true}).then(() => { expect(WebMidi.sysexEnabled).to.equal(true); done(); }); }); it("should enable sysex only if requested (legacy)", function(done) { WebMidi.enable(function () { expect(WebMidi.sysexEnabled).to.equal(true); done(); }, true); }); it("should not enable sysex unless requested", function(done) { WebMidi.enable().then(() => { expect(WebMidi.sysexEnabled).to.equal(false); done(); }); }); it("should not enable sysex unless requested (legacy)", function(done) { WebMidi.enable(function () { expect(WebMidi.sysexEnabled).to.equal(false); done(); }); }); it("should continue to support the legacy properties", function(done) { WebMidi.enable(function () { if (WebMidi.supported) { expect(WebMidi.enabled).to.equal(true); expect(WebMidi.sysexEnabled).to.equal(true); } else { expect(WebMidi.enabled).to.equal(false); } done(); }, true); }); it("should dispatch 'enabled' event", function (done) { // Arrange this.timeout(10000); // Assert WebMidi.addListener("enabled", () => done()); WebMidi.enable(); }); it("should dispatch 'midiaccessgranted' event", function(done) { // Arrange WebMidi.addListener("midiaccessgranted", () => done()); // Act WebMidi.enable(); }); }); describe("getInputById()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should throw error if WebMidi is disabled", async function() { await WebMidi.disable(); try { WebMidi.getInputById("test"); return Promise.reject(); } catch (err) { return Promise.resolve(); } }); it("should return undefined if no device is found", function() { expect(WebMidi.getInputById("0000000")).to.equal(undefined); }); it("should return the right input", function() { if (WebMidi.inputs.length > 0) { let id = WebMidi.inputs[0].id; expect(WebMidi.getInputById(id)).to.equal(WebMidi.inputs[0]); } else { this.skip(); } }); it("should return an instance of 'Input' class", function() { if (WebMidi.inputs.length > 0) { expect(WebMidi.getInputById(WebMidi.inputs[0].id)) .to.be.instanceOf(WebMidi.inputs[0].constructor); } else { this.skip(); } }); it("should return 'undefined' when invalid id is provided", function() { if (WebMidi.inputs.length > 0) { [null, undefined, "", [], {}].forEach(id => { expect(WebMidi.getInputById(id)).to.equal(undefined); }); } else { this.skip(); } }); }); describe("getInputByName()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should throw error if WebMidi is disabled", async function() { await WebMidi.disable(); try { WebMidi.getInputByName("test"); return Promise.reject(); } catch (err) { return Promise.resolve(); } }); it("should return undefined if no device is found", function() { expect(WebMidi.getInputByName("0000000")).to.equal(undefined); }); it("should return the right input", function() { if (WebMidi.inputs.length > 0) { let name = WebMidi.inputs[0].name; expect(WebMidi.getInputByName(name).name).to.equal(name); } else { this.skip(); } }); it("should return instance of 'Input' class", function() { if (WebMidi.inputs.length > 0) { expect(WebMidi.getInputByName(WebMidi.inputs[WebMidi.inputs.length - 1].name)) .to.be.instanceOf(WebMidi.inputs[WebMidi.inputs.length - 1].constructor); } else { this.skip(); } }); it("should return 'undefined' when an invalid name is provided", function() { if (WebMidi.inputs.length > 0) { [null, undefined, ""].forEach(name => { expect(WebMidi.getInputByName(name)).to.equal(undefined); }); } else { this.skip(); } }); }); describe("getOutputById()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should throw error if WebMidi is disabled", async function() { await WebMidi.disable(); try { WebMidi.getOutputById("test"); return Promise.reject(); } catch (err) { return Promise.resolve(); } }); it("should return undefined if no device is found", function() { expect(WebMidi.getOutputById("0000000")).to.equal(undefined); }); it("should return the right output", function() { if (WebMidi.outputs.length > 0) { var id = WebMidi.outputs[0].id; expect(WebMidi.getOutputById(id)).to.equal(WebMidi.outputs[0]); } else { this.skip(); } }); it("should return an instance of the Output class", function() { if (WebMidi.outputs.length > 0) { expect(WebMidi.getOutputById(WebMidi.outputs[0].id)) .to.be.instanceOf(WebMidi.outputs[0].constructor); } else { this.skip(); } }); it("should return 'undefined' when a weird id is provided", function() { if (WebMidi.inputs.length > 0) { [null, undefined, "", [], {}].forEach(id => { expect(WebMidi.getOutputById(id)).to.equal(undefined); }); } else { this.skip(); } }); }); describe("getOutputByName()", function() { beforeEach("Enable WebMidi", async function() { await WebMidi.enable({sysex: true}); }); it("should throw error if WebMidi is disabled", async function() { await WebMidi.disable(); try { WebMidi.getOutputByName("test"); return Promise.reject(); } catch (err) { return Promise.resolve(); } }); it("should return undefined if no device is found", function() { expect(WebMidi.getOutputByName("0000000")).to.equal(undefined); }); it("should return the right output", function() { if (WebMidi.outputs.length > 0) { var name = WebMidi.outputs[0].name; expect(WebMidi.getOutputByName(name).name).to.equal(WebMidi.outputs[0].name); } else { this.skip(); } }); it("should return instance of Output class", function() { if (WebMidi.outputs.length > 0) { expect(WebMidi.getOutputByName(WebMidi.outputs[WebMidi.outputs.length - 1].name)) .to.be.instanceOf(WebMidi.outputs[WebMidi.outputs.length - 1].constructor); } else { this.skip(); } }); it("should return 'undefined' when an invalid name is provided", function() { if (WebMidi.outputs.length > 0) { [null, undefined, ""].forEach(name => { expect(WebMidi.getOutputByName(name)).to.equal(undefined); }); } else { this.skip(); } }); }); describe("getOctave()", function() { it("should return false if number is invalid", function() { ["abc", null, undefined, -1, 128, () => {}, {}, "555", Infinity, -Infinity] .forEach(function (param) { expect(WebMidi.getOctave(param)).to.be.false; }); }); it("should return a signed integer", function() { for (let i = 0; i <= 127; i++) { let result = WebMidi.getOctave(i); expect(result % 1).to.be.equal(0); } }); it("should place MIDI note number 60 (C4/middle C) at octave 4", function() { expect(WebMidi.getOctave(60)).to.be.equal(4); }); it("should take into account desired 'octaveOffset' value", function() { WebMidi.octaveOffset = 2; expect(WebMidi.getOctave(60)).to.be.equal(6); WebMidi.octaveOffset = -4; expect(WebMidi.getOctave(60)).to.be.equal(0); WebMidi.octaveOffset = 0; }); }); describe("noteNameToNumber()", function() { it("should return the same as Utilities.toNoteNumber()", function() { // Arrange const items = [ "C-1", "Cb4", "C4", "C#4", "G9" ]; // Act items.forEach(param => { expect(WebMidi.noteNameToNumber(param)).to.equal(Utilities.toNoteNumber(param)); }); }); it("should return the correct number given the current octave", function() { expect(Utilities.toNoteNumber("C-1", 0)).to.equal(0); expect(Utilities.toNoteNumber("C-1", 1)).to.equal(12); expect(Utilities.toNoteNumber("C4", 0)).to.equal(60); expect(Utilities.toNoteNumber("C4", -1)).to.equal(48); expect(Utilities.toNoteNumber("C4", 1)).to.equal(72); expect(Utilities.toNoteNumber("G8", 1)).to.equal(127); }); }); describe("set octaveOffset()", function() { it("should be tested!"); }); describe("toMIDIChannels()", function() { it("should return the same as sanitizeChannels()", function() { let values = [ 1, -1, -2.3, 0, 17, "x", NaN, null, Infinity, 8, -Infinity, true, false, [undefined], 4e2, 16, 1, 16 ]; let a = WebMidi.toMIDIChannels(values); let b = Utilities.sanitizeChannels(values); for (let i = 0; i {}, {}, Infinity, -Infinity].forEach(value => { expect(() => WebMidi.octaveOffset = value).to.throw(Error); }); }); it("should properly set the octave when the value is valid", function() { [-1, 9, "-1", "9"].forEach(value => { WebMidi.octaveOffset = value; expect(WebMidi._octaveOffset).to.equal(WebMidi.octaveOffset); }); }); }); describe("get supported()", function() { it("should return true if midi available", function(done) { WebMidi.enable().then(() => { expect(WebMidi.supported).to.be.true; done(); }); }); }); }); ================================================ FILE: test/support/JZZ.js ================================================ !function(t,e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define("JZZ",[],e);else{if((t=t||window).JZZ&&t.JZZ.MIDI)return;t.JZZ=e()}}(this,function(){var o,r,n,t,i,e="undefined"==typeof window?global:window,s="1.0.3",u=Date.now||function(){return(new Date).getTime()},a=u(),h="undefined"!=typeof performance&&performance.now?function(){return performance.now()}:function(){return u()-a},f=function(t){setTimeout(t,0)};function c(){(this._orig=this)._ready=!1,this._queue=[],this._log=[]}function p(t,e){this._bad?e instanceof Function&&e.apply(this,[new Error(this._err())]):t instanceof Function&&t.apply(this,[this])}function l(t,e){this._bad?t._crash(this._err()):setTimeout(function(){t._resume()},e)}function d(t){this._bad&&t._break(this._err()),t._resume()}function _(t,n,r){t[r]=function(){var t=arguments,e=n._image();return this._push(d,[e]),e[r].apply(e,t)}}function m(t){this._bad||(t instanceof Function?t.apply(this):console.log(t))}function g(t){this._bad&&(t instanceof Function?t.apply(this):console.log(t))}function v(t){this._bad?t._crash(this._err()):(this._break("Closed"),t._resume())}function y(t){if(t.length){var e=t.shift();if(t.length){var n=this;this._slip(g,[function(){y.apply(n,[t])}])}try{this._repair(),e.apply(this)}catch(t){this._break(t.toString())}}else this._break()}function C(t,e){for(var n=0;n>1&3],(1&t[7])<<4|t[6],t[5]<<4|t[4],t[3]<<4|t[2],t[1]<<4|t[0]]}function pt(t){for(var e,n=[],r=0;r=this.type&&(this.frame=this.type-1),this},ht.prototype.setHour=function(t){if(void 0===t&&(t=0),t!=parseInt(t)||t<0||24<=t)throw RangeError("Bad SMPTE hours value: "+t);return this.hour=t,this},ht.prototype.setMinute=function(t){if(void 0===t&&(t=0),t!=parseInt(t)||t<0||60<=t)throw RangeError("Bad SMPTE minutes value: "+t);return this.minute=t,ft.apply(this),this},ht.prototype.setSecond=function(t){if(void 0===t&&(t=0),t!=parseInt(t)||t<0||60<=t)throw RangeError("Bad SMPTE seconds value: "+t);return this.second=t,ft.apply(this),this},ht.prototype.setFrame=function(t){if(void 0===t&&(t=0),t!=parseInt(t)||t<0||t>=this.type)throw RangeError("Bad SMPTE frame number: "+t);return this.frame=t,ft.apply(this),this},ht.prototype.setQuarter=function(t){if(void 0===t&&(t=0),t!=parseInt(t)||t<0||8<=t)throw RangeError("Bad SMPTE quarter frame: "+t);return this.quarter=t,this},ht.prototype.incrFrame=function(){return this.frame++,this.frame>=this.type&&(this.frame=0,this.second++,60<=this.second&&(this.second=0,this.minute++,60<=this.minute&&(this.minute=0,this.hour=23<=this.hour?0:this.hour+1))),ft.apply(this),this},ht.prototype.decrFrame=function(){return!this.second&&2==this.frame&&29.97==this.type&&this.minute%10&&(this.frame=0),this.frame--,this.frame<0&&(this.frame=29.97==this.type?29:this.type-1,this.second--,this.second<0&&(this.second=59,this.minute--,this.minute<0&&(this.minute=59,this.hour=this.hour?this.hour-1:23))),this},ht.prototype.incrQF=function(){return this.backwards=!1,this.quarter=this.quarter+1&7,0!=this.quarter&&4!=this.quarter||this.incrFrame(),this},ht.prototype.decrQF=function(){return this.backwards=!0,this.quarter=this.quarter+7&7,3!=this.quarter&&7!=this.quarter||this.decrFrame(),this},ht.prototype.read=function(t){if(t instanceof lt||(t=lt.apply(null,arguments)),240==t[0]&&127==t[1]&&1==t[3]&&1==t[4]&&247==t[9])return this.type=[24,25,29.97,30][t[5]>>5&3],this.hour=31&t[5],this.minute=t[6],this.second=t[7],this.frame=t[8],this.quarter=0,this._=void 0,this._b=void 0,!(this._f=void 0);if(241!=t[0]||void 0===t[1])return!1;var e=t[1]>>4,n=15&t[1];return 0==e?7==this._&&(7==this._f&&(this.reset(ct(this._a)),this.incrFrame()),this.incrFrame()):3==e?4==this._&&this.decrFrame():4==e?3==this._&&this.incrFrame():7==e&&0===this._&&(0===this._b&&(this.reset(ct(this._a)),this.decrFrame()),this.decrFrame()),this._a||(this._a=[]),this._a[e]=n,this._f=this._f===e-1||0==e?e:void 0,this._b=this._b===1+e||7==e?e:void 0,this._=e,this.quarter=e,!0},ht.prototype.toString=function(){return pt([this.hour,this.minute,this.second,this.frame])},at.SMPTE=ht;var dt={};((lt.prototype=[]).constructor=lt).noteValue=function(t){return void 0===t?void 0:dt[t.toString().toLowerCase()]},lt.programValue=function(t){return t},lt.freq=function(t,e){return void 0===e&&(e=440),e*Math.pow(2,(vt(lt.noteValue(t),t)-69)/12)};var _t={c:0,d:2,e:4,f:5,g:7,a:9,b:11,h:11};for(n in _t)if(_t.hasOwnProperty(n))for(i=0;i<12&&!(127<(t=_t[n]+12*i));i++)0<(dt[n+i]=t)&&(dt[n+"b"+i]=t-1,dt[n+"bb"+i]=t-2),t<127&&(dt[n+"#"+i]=t+1,dt[n+"##"+i]=t+2);for(i=0;i<128;i++)dt[i]=i;function mt(t){throw RangeError("Bad MIDI value: "+t)}function gt(t){return U(t),parseInt(t)}function vt(t,e){return(t!=parseInt(t)||t<0||127>7}var wt={noteOff:function(t,e,n){return void 0===n&&(n=64),[128+gt(t),vt(lt.noteValue(e),e),vt(n)]},noteOn:function(t,e,n){return void 0===n&&(n=127),[144+gt(t),vt(lt.noteValue(e),e),vt(n)]},aftertouch:function(t,e,n){return[160+gt(t),vt(lt.noteValue(e),e),vt(n)]},control:function(t,e,n){return[176+gt(t),vt(e),vt(n)]},program:function(t,e){return[192+gt(t),vt(lt.programValue(e),e)]},pressure:function(t,e){return[208+gt(t),vt(e)]},pitchBend:function(t,e){return[224+gt(t),Ct(e),bt(e)]},bankMSB:function(t,e){return[176+gt(t),0,vt(e)]},bankLSB:function(t,e){return[176+gt(t),32,vt(e)]},modMSB:function(t,e){return[176+gt(t),1,vt(e)]},modLSB:function(t,e){return[176+gt(t),33,vt(e)]},breathMSB:function(t,e){return[176+gt(t),2,vt(e)]},breathLSB:function(t,e){return[176+gt(t),34,vt(e)]},footMSB:function(t,e){return[176+gt(t),4,vt(e)]},footLSB:function(t,e){return[176+gt(t),36,vt(e)]},portamentoMSB:function(t,e){return[176+gt(t),5,vt(e)]},portamentoLSB:function(t,e){return[176+gt(t),37,vt(e)]},volumeMSB:function(t,e){return[176+gt(t),7,vt(e)]},volumeLSB:function(t,e){return[176+gt(t),39,vt(e)]},balanceMSB:function(t,e){return[176+gt(t),8,vt(e)]},balanceLSB:function(t,e){return[176+gt(t),40,vt(e)]},panMSB:function(t,e){return[176+gt(t),10,vt(e)]},panLSB:function(t,e){return[176+gt(t),42,vt(e)]},expressionMSB:function(t,e){return[176+gt(t),11,vt(e)]},expressionLSB:function(t,e){return[176+gt(t),43,vt(e)]},damper:function(t,e){return[176+gt(t),64,e?127:0]},portamento:function(t,e){return[176+gt(t),65,e?127:0]},sostenuto:function(t,e){return[176+gt(t),66,e?127:0]},soft:function(t,e){return[176+gt(t),67,e?127:0]},allSoundOff:function(t){return[176+gt(t),120,0]},allNotesOff:function(t){return[176+gt(t),123,0]}},St={mtc:function(t){return[241,function(t){var e;switch(!t.backwards&&4<=t.quarter?t.decrFrame():t.backwards&&t.quarter<4&&t.incrFrame(),t.quarter>>1){case 0:e=t.frame;break;case 1:e=t.second;break;case 2:e=t.minute;break;default:e=t.hour}return 1&t.quarter?e>>=4:e&=15,7==t.quarter&&(25==t.type?e|=2:29.97==t.type?e|=4:30==t.type&&(e|=6)),!t.backwards&&4<=t.quarter?t.incrFrame():t.backwards&&t.quarter<4&&t.decrFrame(),e|t.quarter<<4}(t)]},songPosition:function(t){return[242,Ct(t),bt(t)]},songSelect:function(t){return[243,vt(t)]},tune:function(){return[246]},clock:function(){return[248]},start:function(){return[250]},continue:function(){return[251]},stop:function(){return[252]},active:function(){return[254]},sxIdRequest:function(){return[240,126,127,6,1,247]},sxFullFrame:function(t){return[240,127,127,1,1,function(t){return 25==t.type?32|t.hour:29.97==t.type?64|t.hour:30==t.type?96|t.hour:t.hour}(t),t.getMinute(),t.getSecond(),t.getFrame(),247]},reset:function(){return[255]}};function Mt(t,e){var n=new lt;return n.ff=yt(t),n.dd=void 0===e?"":function(t){t=""+t;for(var e=0;e>8)+String.fromCharCode(255&t)}else if(0==(t=""+t).length)t="\0\0";else if(1==t.length)t="\0"+t;else if(2>16)+String.fromCharCode(t>>8&255)+String.fromCharCode(255&t));throw RangeError("Out of range: "+kt(Dt(t)))},smfBPM:function(t){return Ot.smfTempo(Math.round(6e7/t))},smfSMPTE:function(t){if(t instanceof ht)return Mt(84,String.fromCharCode(t.hour)+String.fromCharCode(t.minute)+String.fromCharCode(t.second)+String.fromCharCode(t.frame)+String.fromCharCode(t.quarter%4*25));if(5==(""+t).length)return Mt(84,t);var e=t instanceof Array?t:Array.prototype.slice.call(arguments);return e.splice(0,0,30),Ot.smfSMPTE(new ht(e))},smfTimeSignature:function(t,e,n,r){var i,o,s,u,a=(""+t).match(/^\s*(\d+)\s*\/\s*(\d+)\s*$/);if(a){if(i=parseInt(a[1]),o=parseInt(a[2]),0>=1;s;s>>=1)o++;return s=e==parseInt(e)?e:24,u=n==parseInt(n)?n:8,Mt(88,String.fromCharCode(i)+String.fromCharCode(o)+String.fromCharCode(s)+String.fromCharCode(u))}if(4==(""+t).length)return Mt(88,t)}else{if(t==parseInt(t)&&e==parseInt(e)&&e&&!(e&e-1)){for(i=t,o=0,s=e,s>>=1;s;s>>=1)o++;return s=n==parseInt(n)?n:24,u=r==parseInt(r)?r:8,Mt(88,String.fromCharCode(i)+String.fromCharCode(o)+String.fromCharCode(s)+String.fromCharCode(u))}if(4==(""+t).length)return Mt(88,t)}throw RangeError("Wrong time signature: "+kt(Dt(t)))},smfKeySignature:function(t){var e=(t=""+t).match(/^\s*([A-H][b#]?)\s*(|maj|major|dur|m|min|minor|moll)\s*$/i);if(e){var n={CB:0,GB:1,DB:2,AB:3,EB:4,BB:5,F:6,C:7,G:8,D:9,A:10,E:11,B:12,H:12,"F#":13,"C#":14,"G#":15,"D#":16,"A#":17}[e[1].toUpperCase()],r={"":0,MAJ:0,MAJOR:0,DUR:0,M:1,MIN:1,MINOR:1,MOLL:1}[e[2].toUpperCase()];void 0!==n&&void 0!==r&&(r&&(n-=3),-7<=(n-=7)&&n<0?t=String.fromCharCode(256+n)+String.fromCharCode(r):0<=n&&n<=7&&(t=String.fromCharCode(n)+String.fromCharCode(r)))}if(2==t.length&&t.charCodeAt(1)<=1&&(t.charCodeAt(0)<=7||t.charCodeAt(0)<=255&&249<=t.charCodeAt(0)))return Mt(89,t);throw RangeError("Incorrect key signature: "+kt(t))},smfSequencer:function(t){return Mt(127,Dt(t))}};function It(t,e,n){t.prototype[e]=function(){return this.send(n.apply(0,arguments)),this}}function At(t,e,n){t.prototype[e]=function(){return this.send(n.apply(0,[this._chan].concat(Array.prototype.slice.call(arguments)))),this}}function Et(t,e){lt[t]=function(){return new lt(e.apply(0,arguments))}}function xt(t,e){lt[t]=function(){return e.apply(0,arguments)}}function Pt(t,r){Et(t,r),Z.prototype[t]=function(){var t,e=Array.prototype.slice.call(arguments);e.length>4;return(e={8:"Note Off",10:"Aftertouch",12:"Program Change",13:"Channel Aftertouch",14:"Pitch Wheel"}[i])?t+" -- "+e:9==i?t+" -- "+(this[2]?"Note On":"Note Off"):11!=i?t:t+" -- "+(e=(e={0:"Bank Select MSB",1:"Modulation Wheel MSB",2:"Breath Controller MSB",4:"Foot Controller MSB",5:"Portamento Time MSB",6:"Data Entry MSB",7:"Channel Volume MSB",8:"Balance MSB",10:"Pan MSB",11:"Expression Controller MSB",12:"Effect Control 1 MSB",13:"Effect Control 2 MSB",16:"General Purpose Controller 1 MSB",17:"General Purpose Controller 2 MSB",18:"General Purpose Controller 3 MSB",19:"General Purpose Controller 4 MSB",32:"Bank Select LSB",33:"Modulation Wheel LSB",34:"Breath Controller LSB",36:"Foot Controller LSB",37:"Portamento Time LSB",38:"Data Entry LSB",39:"Channel Volume LSB",40:"Balance LSB",42:"Pan LSB",43:"Expression Controller LSB",44:"Effect control 1 LSB",45:"Effect control 2 LSB",48:"General Purpose Controller 1 LSB",49:"General Purpose Controller 2 LSB",50:"General Purpose Controller 3 LSB",51:"General Purpose Controller 4 LSB",64:"Damper Pedal On/Off",65:"Portamento On/Off",66:"Sostenuto On/Off",67:"Soft Pedal On/Off",68:"Legato Footswitch",69:"Hold 2",70:"Sound Controller 1",71:"Sound Controller 2",72:"Sound Controller 3",73:"Sound Controller 4",74:"Sound Controller 5",75:"Sound Controller 6",76:"Sound Controller 7",77:"Sound Controller 8",78:"Sound Controller 9",79:"Sound Controller 10",80:"General Purpose Controller 5",81:"General Purpose Controller 6",82:"General Purpose Controller 7",83:"General Purpose Controller 8",84:"Portamento Control",88:"High Resolution Velocity Prefix",91:"Effects 1 Depth",92:"Effects 2 Depth",93:"Effects 3 Depth",94:"Effects 4 Depth",95:"Effects 5 Depth",96:"Data Increment",97:"Data Decrement",98:"Non-Registered Parameter Number LSB",99:"Non-Registered Parameter Number MSB",100:"Registered Parameter Number LSB",101:"Registered Parameter Number MSB",120:"All Sound Off",121:"Reset All Controllers",122:"Local Control On/Off",123:"All Notes Off",124:"Omni Mode Off",125:"Omni Mode On",126:"Mono Mode On",127:"Poly Mode On"}[this[1]])||"Undefined")},lt.prototype._stamp=function(t){return this._from.push(t._orig?t._orig:t),this},lt.prototype._unstamp=function(t){if(void 0===t)this._from=[];else{t._orig&&(t=t._orig);var e=this._from.indexOf(t);-1this[n].notes.length&&(i=this[e=n].notes.length),r=0;r>4,n=(15&i)<<4|(o=Ut.indexOf(t.charAt(a++)))>>2,r=(3&o)<<6|(s=Ut.indexOf(t.charAt(a++))),u+=String.fromCharCode(e),64!=o&&(u+=String.fromCharCode(n)),64!=s&&(u+=String.fromCharCode(r));return u},at.lib.toBase64=function(t){var e,n,r,i,o,s=0,u=0,a="",h=[];if(!t)return t;for(;e=(o=t.charCodeAt(s++)<<16|t.charCodeAt(s++)<<8|t.charCodeAt(s++))>>18&63,n=o>>12&63,r=o>>6&63,i=63&o,h[u++]=Ut.charAt(e)+Ut.charAt(n)+Ut.charAt(r)+Ut.charAt(i),s=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;n+=63&r,i+=String.fromCharCode(n)}else if(224==(240&n)){if(n=(15&n)<<12,++e>=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;if(n+=(63&r)<<6,++e>=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;n+=63&r,i+=String.fromCharCode(n)}else if(240==(248&n)){if(n=(7&n)<<18,++e>=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;if(n+=(63&r)<<12,++e>=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;if(n+=(63&r)<<6,++e>=t.length)return t;if(128!=(192&(r=t.charCodeAt(e))))return t;if(1114111<(n+=63&r))return t;n-=65536,i+=String.fromCharCode(55296+(n>>10)),i+=String.fromCharCode(56320+(1023&n))}}return i},at.lib.toUTF8=function(t){t=void 0===t?"":""+t;var e,n,r="";for(e=0;e>6)):(n<65536?r+=String.fromCharCode(224+(n>>12)):(r+=String.fromCharCode(240+(n>>18)),r+=String.fromCharCode(128+(n>>12&63))),r+=String.fromCharCode(128+(n>>6&63))),r+=String.fromCharCode(128+(63&n)));return r};var Gt=[],Zt={},Vt={},Jt=e.Promise;function Wt(t,e,n){this.name=t,this.message=e,this.code=n}function Ht(t,e){this.bubbles=!1,this.cancelBubble=!1,this.cancelable=!1,this.currentTarget=e,this.defaultPrevented=!1,this.eventPhase=0,this.path=[],this.port=t,this.returnValue=!0,this.srcElement=e,this.target=e,this.timeStamp=h(),this.type="statechange"}function Qt(t,e){this.bubbles=!1,this.cancelBubble=!1,this.cancelable=!1,this.currentTarget=t,this.data=e,this.defaultPrevented=!1,this.eventPhase=0,this.path=[],this.receivedTime=h(),this.returnValue=!0,this.srcElement=t,this.target=t,this.timeStamp=this.receivedTime,this.type="midimessage"}function Kt(t,e){t&&(t.onstatechange&&t.onstatechange(new Ht(t,t)),e.onstatechange&&e.onstatechange(new Ht(t,e)))}function $t(n,r){var i=this,o=!1,e=null,s=null;this.type="input",this.id=r.id,this.name=r.name,this.manufacturer=r.man,this.version=r.ver,Object.defineProperty(this,"state",{get:function(){return r.connected?"connected":"disconnected"},enumerable:!0}),Object.defineProperty(this,"connection",{get:function(){return o?r.proxy?"open":"pending":"closed"},enumerable:!0}),Object.defineProperty(this,"onmidimessage",{get:function(){return s},set:function(t){t instanceof Function?(s=t,o||i.open()):s=null},enumerable:!0}),Object.defineProperty(this,"onstatechange",{get:function(){return e},set:function(t){e=t instanceof Function?t:null},enumerable:!0}),this.open=function(){return new Jt(function(t,e){o?t(i):r.open().then(function(){o||(o=!0,Kt(i,n)),t(i)},function(){e(new Wt("InvalidAccessError","Port is not available",15))})})},this.close=function(){return new Jt(function(t){o&&(o=!1,r.close(),Kt(i,n)),t(i)})},Object.freeze(this)}function Xt(t){for(var e,n;t.length;){for(e=0;et.length)return;for(e=1;e // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // The type definitions for the native 'Navigator' interface and 'WebMidiApi' namespace have been // created by Toshiya Nakakura . I am copying them here until I figure // out how to rename a TypeScript namespace upon import. The problem is that the original 'WebMidi' // namespace collides with the 'WebMidi' object. So, I simply renamed the namespace to 'WebMidiApi' // and adjusted the references accordingly. interface Navigator { /** * When invoked, returns a Promise object representing a request for access to MIDI devices on the * user's system. */ requestMIDIAccess(options?: WebMidiApi.MIDIOptions): Promise; } declare namespace WebMidiApi { interface MIDIOptions { /** * This member informs the system whether the ability to send and receive system * exclusive messages is requested or allowed on a given MIDIAccess object. */ sysex: boolean; } /** * This is a maplike interface whose value is a MIDIInput instance and key is its * ID. */ type MIDIInputMap = Map; /** * This is a maplike interface whose value is a MIDIOutput instance and key is its * ID. */ type MIDIOutputMap = Map; interface MIDIAccess extends EventTarget { /** * The MIDI input ports available to the system. */ inputs: MIDIInputMap; /** * The MIDI output ports available to the system. */ outputs: MIDIOutputMap; /** * The handler called when a new port is connected or an existing port changes the * state attribute. */ onstatechange(e: MIDIConnectionEvent): void; addEventListener( type: 'statechange', listener: (this: this, e: MIDIConnectionEvent) => any, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void; /** * This attribute informs the user whether system exclusive support is enabled on * this MIDIAccess. */ sysexEnabled: boolean; } type MIDIPortType = 'input' | 'output'; type MIDIPortDeviceState = 'disconnected' | 'connected'; type MIDIPortConnectionState = 'open' | 'closed' | 'pending'; interface MIDIPort extends EventTarget { /** * A unique ID of the port. This can be used by developers to remember ports the * user has chosen for their application. */ id: string; /** * The manufacturer of the port. */ manufacturer?: string | undefined; /** * The system name of the port. */ name?: string | undefined; /** * A descriptor property to distinguish whether the port is an input or an output * port. */ type: MIDIPortType; /** * The version of the port. */ version?: string | undefined; /** * The state of the device. */ state: MIDIPortDeviceState; /** * The state of the connection to the device. */ connection: MIDIPortConnectionState; /** * The handler called when an existing port changes its state or connection * attributes. */ onstatechange(e: MIDIConnectionEvent): void; addEventListener( type: 'statechange', listener: (this: this, e: MIDIConnectionEvent) => any, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void; /** * Makes the MIDI device corresponding to the MIDIPort explicitly available. Note * that this call is NOT required in order to use the MIDIPort - calling send() on * a MIDIOutput or attaching a MIDIMessageEvent handler on a MIDIInputPort will * cause an implicit open(). * * When invoked, this method returns a Promise object representing a request for * access to the given MIDI port on the user's system. */ open(): Promise; /** * Makes the MIDI device corresponding to the MIDIPort * explicitly unavailable (subsequently changing the state from "open" to * "connected"). Note that successful invocation of this method will result in MIDI * messages no longer being delivered to MIDIMessageEvent handlers on a * MIDIInputPort (although setting a new handler will cause an implicit open()). * * When invoked, this method returns a Promise object representing a request for * access to the given MIDI port on the user's system. When the port has been * closed (and therefore, in exclusive access systems, the port is available to * other applications), the vended Promise is resolved. If the port is * disconnected, the Promise is rejected. */ close(): Promise; } interface MIDIInput extends MIDIPort { type: 'input'; onmidimessage(e: MIDIMessageEvent): void; addEventListener( type: 'midimessage', listener: (this: this, e: MIDIMessageEvent) => any, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: 'statechange', listener: (this: this, e: MIDIConnectionEvent) => any, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void; } interface MIDIOutput extends MIDIPort { type: 'output'; /** * Enqueues the message to be sent to the corresponding MIDI port. * @param data The data to be enqueued, with each sequence entry representing a single byte of data. * @param timestamp The time at which to begin sending the data to the port. If timestamp is set * to zero (or another time in the past), the data is to be sent as soon as * possible. */ send(data: number[] | Uint8Array, timestamp?: number): void; /** * Clears any pending send data that has not yet been sent from the MIDIOutput 's * queue. The implementation will need to ensure the MIDI stream is left in a good * state, so if the output port is in the middle of a sysex message, a sysex * termination byte (0xf7) should be sent. */ clear(): void; } interface MIDIMessageEvent extends Event { /** * A timestamp specifying when the event occurred. */ receivedTime: number; /** * A Uint8Array containing the MIDI data bytes of a single MIDI message. */ data: Uint8Array; } interface MIDIMessageEventInit extends EventInit { /** * A timestamp specifying when the event occurred. */ receivedTime: number; /** * A Uint8Array containing the MIDI data bytes of a single MIDI message. */ data: Uint8Array; } interface MIDIConnectionEvent extends Event { /** * The port that has been connected or disconnected. */ port: MIDIPort; } interface MIDIConnectionEventInit extends EventInit { /** * The port that has been connected or disconnected. */ port: MIDIPort; } } /** * The `EventEmitter` class provides methods to implement the _observable_ design pattern. This * pattern allows one to _register_ a function to execute when a specific event is _emitted_ by the * emitter. * * It is intended to be an abstract class meant to be extended by (or mixed into) other objects. */ export declare class EventEmitter { /** * Identifier (Symbol) to use when adding or removing a listener that should be triggered when any * events occur. * * @type {Symbol} */ static get ANY_EVENT(): Symbol; /** * Creates a new `EventEmitter`object. * * @param {boolean} [eventsSuspended=false] Whether the `EventEmitter` is initially in a suspended * state (i.e. not executing callbacks). */ constructor(eventsSuspended?: boolean); /** * An object containing a property for each event with at least one registered listener. Each * event property contains an array of all the [`Listener`]{@link Listener} objects registered * for the event. * * @type {Object} * @readonly */ eventMap: any; /** * Whether or not the execution of callbacks is currently suspended for this emitter. * * @type {boolean} */ eventsSuspended: boolean; /** * Adds a listener for the specified event. It returns the [`Listener`]{@link Listener} object * that was created and attached to the event. * * To attach a global listener that will be triggered for any events, use * [`EventEmitter.ANY_EVENT`]{@link #ANY_EVENT} as the first parameter. Note that a global * listener will also be triggered by non-registered events. * * @param {string|Symbol} event The event to listen to. * @param {EventEmitterCallback} callback The callback function to execute when the event occurs. * @param {Object} [options={}] * @param {Object} [options.context=this] The value of `this` in the callback function. * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus executed first. * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`]{@link Listener#arguments} * property of the [`Listener`]{@link Listener} object and can be retrieved or modified as * desired. * * @returns {Listener} The newly created [`Listener`]{@link Listener} object (typical) or an array * of [`Listener`]{@link Listener} objects. * * @throws {TypeError} The `event` parameter must be a string or * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. * @throws {TypeError} The `callback` parameter must be a function. */ addListener(event: string | Symbol, callback: EventEmitterCallback, options?: { context?: any; prepend?: boolean; duration?: number; remaining?: number; arguments?: any[]; }): Listener | Listener[]; /** * Adds a one-time listener for the specified event. The listener will be executed once and then * destroyed. It returns the [`Listener`]{@link Listener} object that was created and attached * to the event. * * To attach a global listener that will be triggered for any events, use * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the first parameter. Note that a * global listener will also be triggered by non-registered events. * * @param {string|Symbol} event The event to listen to * @param {EventEmitterCallback} callback The callback function to execute when the event occurs * @param {Object} [options={}] * @param {Object} [options.context=this] The context to invoke the callback function in. * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus executed first. * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`]{@link Listener#arguments} * property of the [`Listener`]{@link Listener} object and can be retrieved or modified as * desired. * * @returns {Listener} The newly created [`Listener`]{@link Listener} object (typical) or an array * of [`Listener`]{@link Listener} objects. * * @throws {TypeError} The `event` parameter must be a string or * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. * @throws {TypeError} The `callback` parameter must be a function. */ addOneTimeListener(event: string | Symbol, callback: EventEmitterCallback, options?: { context?: any; prepend?: boolean; duration?: number; arguments?: any[]; }): Listener | Listener[]; /** * Returns `true` if the specified event has at least one registered listener. If no event is * specified, the method returns `true` if any event has at least one listener registered (this * includes global listeners registered to * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}). * * Note: to specifically check for global listeners added with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}, use * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter. * * @param {string|Symbol} [event=(any event)] The event to check * @param {function|Listener} [callback=(any callback)] The actual function that was added to the * event or the {@link Listener} object returned by `addListener()`. * @returns {boolean} */ hasListener(event?: string | Symbol, callback?: Function | Listener): boolean; /** * An array of all the unique event names for which the emitter has at least one registered * listener. * * Note: this excludes global events registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} because they are not tied to a * specific event. * * @type {string[]} * @readonly */ get eventNames(): string[]; /** * Returns an array of all the [`Listener`]{@link Listener} objects that have been registered for * a specific event. * * Please note that global events (those added with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}) are not returned for "regular" * events. To get the list of global listeners, specifically use * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter. * * @param {string|Symbol} event The event to get listeners for. * @returns {Listener[]} An array of [`Listener`]{@link Listener} objects. */ getListeners(event: string | Symbol): Listener[]; /** * Suspends execution of all callbacks functions registered for the specified event type. * * You can suspend execution of callbacks registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} by passing * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} to `suspendEvent()`. Beware that this * will not suspend all callbacks but only those registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. While this may seem counter-intuitive * at first glance, it allows the selective suspension of global listeners while leaving other * listeners alone. If you truly want to suspends all callbacks for a specific * [`EventEmitter`]{@link EventEmitter}, simply set its `eventsSuspended` property to `true`. * * @param {string|Symbol} event The event name (or `EventEmitter.ANY_EVENT`) for which to suspend * execution of all callback functions. */ suspendEvent(event: string | Symbol): void; /** * Resumes execution of all suspended callback functions registered for the specified event type. * * You can resume execution of callbacks registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} by passing * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} to `unsuspendEvent()`. Beware that * this will not resume all callbacks but only those registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. While this may seem * counter-intuitive, it allows the selective unsuspension of global listeners while leaving other * callbacks alone. * * @param {string|Symbol} event The event name (or `EventEmitter.ANY_EVENT`) for which to resume * execution of all callback functions. */ unsuspendEvent(event: string | Symbol): void; /** * Returns the number of listeners registered for a specific event. * * Please note that global events (those added with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}) do not count towards the remaining * number for a "regular" event. To get the number of global listeners, specifically use * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter. * * @param {string|Symbol} event The event which is usually a string but can also be the special * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} symbol. * @returns {number} An integer representing the number of listeners registered for the specified * event. */ getListenerCount(event: string | Symbol): number; /** * Executes the callback function of all the [`Listener`]{@link Listener} objects registered for * a given event. The callback functions are passed the additional arguments passed to `emit()` * (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object (if any). * * If the [`eventsSuspended`]{@link #eventsSuspended} property is `true` or the * [`Listener.suspended`]{@link Listener#suspended} property is `true`, the callback functions * will not be executed. * * This function returns an array containing the return values of each of the callbacks. * * It should be noted that the regular listeners are triggered first followed by the global * listeners (those added with [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}). * * @param {string} event The event * @param {...*} args Arbitrary number of arguments to pass along to the callback functions * * @returns {Array} An array containing the return value of each of the executed listener * functions. * * @throws {TypeError} The `event` parameter must be a string. */ emit(event: string, ...args: any[]): any[]; /** * Removes all the listeners that match the specified criterias. If no parameters are passed, all * listeners will be removed. If only the `event` parameter is passed, all listeners for that * event will be removed. You can remove global listeners by using * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the first parameter. * * To use more granular options, you must at least define the `event`. Then, you can specify the * callback to match or one or more of the additional options. * * @param {string | Symbol} [event] The event name. * @param {EventEmitterCallback} [callback] Only remove the listeners that match this exact * callback function. * @param {Object} [options] * @param {*} [options.context] Only remove the listeners that have this exact context. * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener( event?: string | Symbol, callback?: EventEmitterCallback, options?: { context?: any; remaining?: number; } ): void; /** * The `waitFor()` method is an async function which returns a promise. The promise is fulfilled * when the specified event occurs. The event can be a regular event or * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} (if you want to resolve as soon as any * event is emitted). * * If the `duration` option is set, the promise will only be fulfilled if the event is emitted * within the specified duration. If the event has not been fulfilled after the specified * duration, the promise is rejected. This makes it super easy to wait for an event and timeout * after a certain time if the event is not triggered. * * @param {string|Symbol} event The event to wait for * @param {Object} [options={}] * @param {number} [options.duration=Infinity] The number of milliseconds to wait before the * promise is automatically rejected. */ waitFor(event: string | Symbol, options?: { duration?: number; }): Promise; /** * The number of unique events that have registered listeners. * * Note: this excludes global events registered with * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} because they are not tied to a * specific event. * * @type {number} * @readonly */ get eventCount(): number; } /** * The `Listener` class represents a single event listener object. Such objects keep all relevant * contextual information such as the event being listened to, the object the listener was attached * to, the callback function and so on. * */ export declare class Listener { /** * Creates a new `Listener` object * * @param {string|Symbol} event The event being listened to * @param {EventEmitter} target The [`EventEmitter`]{@link EventEmitter} object that the listener * is attached to. * @param {EventEmitterCallback} callback The function to call when the listener is triggered * @param {Object} [options={}] * @param {Object} [options.context=target] The context to invoke the listener in (a.k.a. the * value of `this` inside the callback function). * @param {number} [options.remaining=Infinity] The remaining number of times after which the * callback should automatically be removed. * @param {array} [options.arguments] An array of arguments that will be passed separately to the * callback function upon execution. The array is stored in the [`arguments`]{@link #arguments} * property and can be retrieved or modified as desired. * * @throws {TypeError} The `event` parameter must be a string or * [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. * @throws {ReferenceError} The `target` parameter is mandatory. * @throws {TypeError} The `callback` must be a function. */ constructor(event: string | Symbol, target: EventEmitter, callback: EventEmitterCallback, options?: { context?: any; remaining?: number; arguments?: any[]; }, ...args: any[]); /** * An array of arguments to pass to the callback function upon execution. * @type {array} */ arguments: any[]; /** * The callback function to execute. * @type {Function} */ callback: Function; /** * The context to execute the callback function in (a.k.a. the value of `this` inside the * callback function) * @type {Object} */ context: any; /** * The number of times the listener function was executed. * @type {number} */ count: number; /** * The event name. * @type {string} */ event: string; /** * The remaining number of times after which the callback should automatically be removed. * @type {number} */ remaining: number; /** * Whether this listener is currently suspended or not. * @type {boolean} */ suspended: boolean; /** * The object that the event is attached to (or that emitted the event). * @type {EventEmitter} */ target: EventEmitter; /** * Removes the listener from its target. */ remove(): void; } /** * The `Enumerations` class contains enumerations and arrays of elements used throughout the * library. All properties are static and should be referenced using the class name. For example: * `Enumerations.CHANNEL_MESSAGES`. * * @license Apache-2.0 * @since 3.0.0 */ export class Enumerations { /** * Enumeration of all MIDI channel message names and their associated 4-bit numerical value: * * | Message Name | Hexadecimal | Decimal | * |---------------------|-------------|---------| * | `noteoff` | 0x8 | 8 | * | `noteon` | 0x9 | 9 | * | `keyaftertouch` | 0xA | 10 | * | `controlchange` | 0xB | 11 | * | `programchange` | 0xC | 12 | * | `channelaftertouch` | 0xD | 13 | * | `pitchbend` | 0xE | 14 | * * @enum {Object.} * @readonly * @since 3.1 * @static */ static get CHANNEL_MESSAGES(): { noteoff: number; noteon: number; keyaftertouch: number; controlchange: number; programchange: number; channelaftertouch: number; pitchbend: number; }; /** * A simple array of the 16 valid MIDI channel numbers (`1` to `16`): * * @type {number[]} * @readonly * @static */ static get CHANNEL_NUMBERS(): number[]; /** * Enumeration of all MIDI channel mode message names and their associated numerical value: * * * | Message Name | Hexadecimal | Decimal | * |-----------------------|-------------|---------| * | `allsoundoff` | 0x78 | 120 | * | `resetallcontrollers` | 0x79 | 121 | * | `localcontrol` | 0x7A | 122 | * | `allnotesoff` | 0x7B | 123 | * | `omnimodeoff` | 0x7C | 124 | * | `omnimodeon` | 0x7D | 125 | * | `monomodeon` | 0x7E | 126 | * | `polymodeon` | 0x7F | 127 | * * @enum {Object.} * @readonly * @static */ static get CHANNEL_MODE_MESSAGES(): { allsoundoff: number; resetallcontrollers: number; localcontrol: number; allnotesoff: number; omnimodeoff: number; omnimodeon: number; monomodeon: number; polymodeon: number; }; /** * An array of objects, ordered by control number, describing control change messages. Each object * in the array can have up to 4 properties: * * * `number`: MIDI control number (0-127); * * `event`: name of emitted event (eg: `bankselectcoarse`, `choruslevel`, etc) that can be * listened to; * * `description`: user-friendly description of the controller's purpose; * * `position`: whether this controller's value should be considered an `msb` or `lsb` (if * appropriate). * * Not all controllers have a predefined function. For those that don't, name is the word * "controller" followed by the number (e.g. `controller112`). * * | Event name | Control Number | * |--------------------------------|----------------| * | `bankselectcoarse` | 0 | * | `modulationwheelcoarse` | 1 | * | `breathcontrollercoarse` | 2 | * | `controller3` | 3 | * | `footcontrollercoarse` | 4 | * | `portamentotimecoarse` | 5 | * | `dataentrycoarse` | 6 | * | `volumecoarse` | 7 | * | `balancecoarse` | 8 | * | `controller9` | 9 | * | `pancoarse` | 10 | * | `expressioncoarse` | 11 | * | `effectcontrol1coarse` | 12 | * | `effectcontrol2coarse` | 13 | * | `controller14` | 14 | * | `controller15` | 15 | * | `generalpurposecontroller1` | 16 | * | `generalpurposecontroller2` | 17 | * | `generalpurposecontroller3` | 18 | * | `generalpurposecontroller4` | 19 | * | `controller20` | 20 | * | `controller21` | 21 | * | `controller22` | 22 | * | `controller23` | 23 | * | `controller24` | 24 | * | `controller25` | 25 | * | `controller26` | 26 | * | `controller27` | 27 | * | `controller28` | 28 | * | `controller29` | 29 | * | `controller30` | 30 | * | `controller31` | 31 | * | `bankselectfine` | 32 | * | `modulationwheelfine` | 33 | * | `breathcontrollerfine` | 34 | * | `controller35` | 35 | * | `footcontrollerfine` | 36 | * | `portamentotimefine` | 37 | * | `dataentryfine` | 38 | * | `channelvolumefine` | 39 | * | `balancefine` | 40 | * | `controller41` | 41 | * | `panfine` | 42 | * | `expressionfine` | 43 | * | `effectcontrol1fine` | 44 | * | `effectcontrol2fine` | 45 | * | `controller46` | 46 | * | `controller47` | 47 | * | `controller48` | 48 | * | `controller49` | 49 | * | `controller50` | 50 | * | `controller51` | 51 | * | `controller52` | 52 | * | `controller53` | 53 | * | `controller54` | 54 | * | `controller55` | 55 | * | `controller56` | 56 | * | `controller57` | 57 | * | `controller58` | 58 | * | `controller59` | 59 | * | `controller60` | 60 | * | `controller61` | 61 | * | `controller62` | 62 | * | `controller63` | 63 | * | `damperpedal` | 64 | * | `portamento` | 65 | * | `sostenuto` | 66 | * | `softpedal` | 67 | * | `legatopedal` | 68 | * | `hold2` | 69 | * | `soundvariation` | 70 | * | `resonance` | 71 | * | `releasetime` | 72 | * | `attacktime` | 73 | * | `brightness` | 74 | * | `decaytime` | 75 | * | `vibratorate` | 76 | * | `vibratodepth` | 77 | * | `vibratodelay` | 78 | * | `controller79` | 79 | * | `generalpurposecontroller5` | 80 | * | `generalpurposecontroller6` | 81 | * | `generalpurposecontroller7` | 82 | * | `generalpurposecontroller8` | 83 | * | `portamentocontrol` | 84 | * | `controller85` | 85 | * | `controller86` | 86 | * | `controller87` | 87 | * | `highresolutionvelocityprefix` | 88 | * | `controller89` | 89 | * | `controller90` | 90 | * | `effect1depth` | 91 | * | `effect2depth` | 92 | * | `effect3depth` | 93 | * | `effect4depth` | 94 | * | `effect5depth` | 95 | * | `dataincrement` | 96 | * | `datadecrement` | 97 | * | `nonregisteredparameterfine` | 98 | * | `nonregisteredparametercoarse` | 99 | * | `nonregisteredparameterfine` | 100 | * | `registeredparametercoarse` | 101 | * | `controller102` | 102 | * | `controller103` | 103 | * | `controller104` | 104 | * | `controller105` | 105 | * | `controller106` | 106 | * | `controller107` | 107 | * | `controller108` | 108 | * | `controller109` | 109 | * | `controller110` | 110 | * | `controller111` | 111 | * | `controller112` | 112 | * | `controller113` | 113 | * | `controller114` | 114 | * | `controller115` | 115 | * | `controller116` | 116 | * | `controller117` | 117 | * | `controller118` | 118 | * | `controller119` | 119 | * | `allsoundoff` | 120 | * | `resetallcontrollers` | 121 | * | `localcontrol` | 122 | * | `allnotesoff` | 123 | * | `omnimodeoff` | 124 | * | `omnimodeon` | 125 | * | `monomodeon` | 126 | * | `polymodeon` | 127 | * * @type {Object[]} * @readonly * @static * @since 3.1 */ static get CONTROL_CHANGE_MESSAGES():object[] /** * Enumeration of all MIDI registered parameters and their associated pair of numerical values. * MIDI registered parameters extend the original list of control change messages. Currently, * there are only a limited number of them: * * * | Control Function | [LSB, MSB] | * |------------------------------|--------------| * | `pitchbendrange` | [0x00, 0x00] | * | `channelfinetuning` | [0x00, 0x01] | * | `channelcoarsetuning` | [0x00, 0x02] | * | `tuningprogram` | [0x00, 0x03] | * | `tuningbank` | [0x00, 0x04] | * | `modulationrange` | [0x00, 0x05] | * | `azimuthangle` | [0x3D, 0x00] | * | `elevationangle` | [0x3D, 0x01] | * | `gain` | [0x3D, 0x02] | * | `distanceratio` | [0x3D, 0x03] | * | `maximumdistance` | [0x3D, 0x04] | * | `maximumdistancegain` | [0x3D, 0x05] | * | `referencedistanceratio` | [0x3D, 0x06] | * | `panspreadangle` | [0x3D, 0x07] | * | `rollangle` | [0x3D, 0x08] | * * @enum {Object.} * @readonly * @static */ static get REGISTERED_PARAMETERS(): { pitchbendrange: number[]; channelfinetuning: number[]; channelcoarsetuning: number[]; tuningprogram: number[]; tuningbank: number[]; modulationrange: number[]; azimuthangle: number[]; elevationangle: number[]; gain: number[]; distanceratio: number[]; maximumdistance: number[]; maximumdistancegain: number[]; referencedistanceratio: number[]; panspreadangle: number[]; rollangle: number[]; }; /** * Enumeration of all valid MIDI system messages and matching numerical values. WebMidi.js also * uses two additional custom messages. * * **System Common Messages** * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `sysex` | 0xF0 | 240 | * | `timecode` | 0xF1 | 241 | * | `songposition` | 0xF2 | 242 | * | `songselect` | 0xF3 | 243 | * | `tunerequest` | 0xF6 | 246 | * | `sysexend` | 0xF7 | 247 | * * The `sysexend` message is never actually received. It simply ends a sysex stream. * * **System Real-Time Messages** * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `clock` | 0xF8 | 248 | * | `start` | 0xFA | 250 | * | `continue` | 0xFB | 251 | * | `stop` | 0xFC | 252 | * | `activesensing` | 0xFE | 254 | * | `reset` | 0xFF | 255 | * * Values 249 and 253 are relayed by the * [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) but they do not * serve any specific purpose. The * [MIDI 1.0 spec](https://www.midi.org/specifications/item/table-1-summary-of-midi-message) * simply states that they are undefined/reserved. * * **Custom WebMidi.js Messages** * * These two messages are mostly for internal use. They are not MIDI messages and cannot be sent * or forwarded. * * | Function | Hexadecimal | Decimal | * |------------------------|-------------|---------| * | `midimessage` | | 0 | * | `unknownsystemmessage` | | -1 | * * @enum {Object.} * @readonly * @static */ static get SYSTEM_MESSAGES(): { sysex: number; timecode: number; songposition: number; songselect: number; tunerequest: number; tuningrequest: number; sysexend: number; clock: number; start: number; continue: number; stop: number; activesensing: number; reset: number; midimessage: number; unknownsystemmessage: number; }; /** * Array of channel-specific event names that can be listened for. This includes channel mode * events and RPN/NRPN events. * * @type {string[]} * @readonly */ static get CHANNEL_EVENTS(): string[]; } /** * The `Forwarder` class allows the forwarding of MIDI messages to predetermined outputs. When you * call its [`forward()`](#forward) method, it will send the specified [`Message`](Message) object * to all the outputs listed in its [`destinations`](#destinations) property. * * If specific channels or message types have been defined in the [`channels`](#channels) or * [`types`](#types) properties, only messages matching the channels/types will be forwarded. * * While it can be manually instantiated, you are more likely to come across a `Forwarder` object as * the return value of the [`Input.addForwarder()`](Input#addForwarder) method. * * @license Apache-2.0 * @since 3.0.0 */ export class Forwarder { /** * Creates a `Forwarder` object. * * @param {Output|Output[]} [destinations=\[\]] An [`Output`](Output) object, or an array of such * objects, to forward the message to. * * @param {object} [options={}] * @param {string|string[]} [options.types=(all messages)] A MIDI message type or an array of such * types (`"noteon"`, `"controlchange"`, etc.), that the specified message must match in order to * be forwarded. If this option is not specified, all types of messages will be forwarded. Valid * messages are the ones found in either * [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) * or [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * A MIDI channel number or an array of channel numbers that the message must match in order to be * forwarded. By default all MIDI channels are included (`1` to `16`). */ constructor(destinations?: Output | Output[], options?: { types?: string | string[]; channels?: number | number[]; }); /** * An array of [`Output`](Output) objects to forward the message to. * @type {Output[]} */ destinations: Output[]; /** * An array of message types (`"noteon"`, `"controlchange"`, etc.) that must be matched in order * for messages to be forwarded. By default, this array includes all * [`Enumerations.SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) and * [`Enumerations.CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @type {string[]} */ types: string[]; /** * An array of MIDI channel numbers that the message must match in order to be forwarded. By * default, this array includes all MIDI channels (`1` to `16`). * @type {number[]} */ channels: number[]; /** * Indicates whether message forwarding is currently suspended or not in this forwarder. * @type {boolean} */ suspended: boolean; /** * Sends the specified message to the forwarder's destination(s) if it matches the specified * type(s) and channel(s). * * @param {Message} message The [`Message`](Message) object to forward. */ forward(message: Message): void; } /** * The `Input` class represents a single MIDI input port. This object is automatically instantiated * by the library according to the host's MIDI subsystem and does not need to be directly * instantiated. Instead, you can access all `Input` objects by referring to the * [`WebMidi.inputs`](WebMidi#inputs) array. You can also retrieve inputs by using methods such as * [`WebMidi.getInputByName()`](WebMidi#getInputByName) and * [`WebMidi.getInputById()`](WebMidi#getInputById). * * Note that a single MIDI device may expose several inputs and/or outputs. * * **Important**: the `Input` class does not directly fire channel-specific MIDI messages * (such as [`noteon`](InputChannel#event:noteon) or * [`controlchange`](InputChannel#event:controlchange), etc.). The [`InputChannel`](InputChannel) * object does that. However, you can still use the * [`Input.addListener()`](#addListener) method to listen to channel-specific events on multiple * [`InputChannel`](InputChannel) objects at once. * * @fires Input#opened * @fires Input#disconnected * @fires Input#closed * @fires Input#midimessage * * @fires Input#sysex * @fires Input#timecode * @fires Input#songposition * @fires Input#songselect * @fires Input#tunerequest * @fires Input#clock * @fires Input#start * @fires Input#continue * @fires Input#stop * @fires Input#activesensing * @fires Input#reset * * @fires Input#unknownmidimessage * * @extends EventEmitter * @license Apache-2.0 */ export class Input extends EventEmitter { /** * Creates an `Input` object. * * @param {WebMidiApi.MIDIInput} midiInput [`MIDIInput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput) * object as provided by the MIDI subsystem (Web MIDI API). */ constructor(midiInput: WebMidiApi.MIDIInput); private _forwarders; private _midiInput; private _octaveOffset; private _onMidiMessage; private _onStateChange; private _parseEvent; /** * Array containing the 16 [`InputChannel`](InputChannel) objects available for this `Input`. The * channels are numbered 1 through 16. * * @type {InputChannel[]} */ channels: InputChannel[]; /** * Adds a forwarder that will forward all incoming MIDI messages matching the criteria to the * specified [`Output`](Output) destination(s). This is akin to the hardware MIDI THRU port, with * the added benefit of being able to filter which data is forwarded. * * @param {Output|Output[]|Forwarder} output An [`Output`](Output) object, a [`Forwarder`](Forwarder) * object or an array of such objects, to forward messages to. * @param {object} [options={}] * @param {string|string[]} [options.types=(all messages)] A message type, or an array of such * types (`noteon`, `controlchange`, etc.), that the message type must match in order to be * forwarded. If this option is not specified, all types of messages will be forwarded. Valid * messages are the ones found in either * [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) or * [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * A MIDI channel number or an array of channel numbers that the message must match in order to be * forwarded. By default all MIDI channels are included (`1` to `16`). * * @returns {Forwarder} The [`Forwarder`](Forwarder) object created to handle the forwarding. This * is useful if you wish to manipulate or remove the [`Forwarder`](Forwarder) later on. */ addForwarder(output: Output | Output[] | Forwarder, options?: { types?: string | string[]; channels?: number | number[]; }): Forwarder; // @ts-ignore /** * Adds an event listener that will trigger a function callback when the specified event is * dispatched. The event usually is **input-wide** but can also be **channel-specific**. * * Input-wide events do not target a specific MIDI channel so it makes sense to listen for them * at the `Input` level and not at the [`InputChannel`](InputChannel) level. Channel-specific * events target a specific channel. Usually, in this case, you would add the listener to the * [`InputChannel`](InputChannel) object. However, as a convenience, you can also listen to * channel-specific events directly on an `Input`. This allows you to react to a channel-specific * event no matter which channel it actually came through. * * When listening for an event, you simply need to specify the event name and the function to * execute: * * ```javascript * const listener = WebMidi.inputs[0].addListener("midimessage", e => { * console.log(e); * }); * ``` * * Calling the function with an input-wide event (such as * [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object * that was created. * * If you call the function with a channel-specific event (such as * [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all * [`Listener`](Listener) objects that were created (one for each channel): * * ```javascript * const listeners = WebMidi.inputs[0].addListener("noteon", someFunction); * ``` * * You can also specify which channels you want to add the listener to: * * ```javascript * const listeners = WebMidi.inputs[0].addListener("noteon", someFunction, {channels: [1, 2, 3]}); * ``` * * In this case, `listeners` is an array containing 3 [`Listener`](Listener) objects. * * Note that, when adding channel-specific listeners, it is the [`InputChannel`](InputChannel) * instance that actually gets a listener added and not the `Input` instance. You can check that * by calling [`InputChannel.hasListener()`](InputChannel#hasListener()). * * There are 8 families of events you can listen to: * * 1. **MIDI System Common** Events (input-wide) * * * [`songposition`]{@link Input#event:songposition} * * [`songselect`]{@link Input#event:songselect} * * [`sysex`]{@link Input#event:sysex} * * [`timecode`]{@link Input#event:timecode} * * [`tunerequest`]{@link Input#event:tunerequest} * * 2. **MIDI System Real-Time** Events (input-wide) * * * [`clock`]{@link Input#event:clock} * * [`start`]{@link Input#event:start} * * [`continue`]{@link Input#event:continue} * * [`stop`]{@link Input#event:stop} * * [`activesensing`]{@link Input#event:activesensing} * * [`reset`]{@link Input#event:reset} * * 3. **State Change** Events (input-wide) * * * [`opened`]{@link Input#event:opened} * * [`closed`]{@link Input#event:closed} * * [`disconnected`]{@link Input#event:disconnected} * * 4. **Catch-All** Events (input-wide) * * * [`midimessage`]{@link Input#event:midimessage} * * [`unknownmidimessage`]{@link Input#event:unknownmidimessage} * * 5. **Channel Voice** Events (channel-specific) * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * 6. **Channel Mode** Events (channel-specific) * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * 7. **NRPN** Events (channel-specific) * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * 8. **RPN** Events (channel-specific) * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event is detected. * This function will receive an event parameter object. For details on this object's properties, * check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to * listen on. If no channel is specified, all channels will be used. This parameter is ignored for * input-wide events. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener|Listener[]} If the event is input-wide, a single [`Listener`](Listener) * object is returned. If the event is channel-specific, an array of all the * [`Listener`](Listener) objects is returned (one for each channel). */ addListener( e: Symbol | T, listener: InputEventMap[T], options?: { "arguments"?: any[]; "channels"?: number | number[]; "context"?: any; "duration"?: number; "prepend"?: boolean; "remaining"?: number; } ): Listener | Listener[]; /** * Adds a one-time event listener that will trigger a function callback when the specified event * happens. The event can be **channel-bound** or **input-wide**. Channel-bound events are * dispatched by [`InputChannel`]{@link InputChannel} objects and are tied to a specific MIDI * channel while input-wide events are dispatched by the `Input` object itself and are not tied * to a specific channel. * * Calling the function with an input-wide event (such as * [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object * that was created. * * If you call the function with a channel-specific event (such as * [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all * [`Listener`](Listener) objects that were created (one for each channel): * * ```javascript * const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction); * ``` * * You can also specify which channels you want to add the listener to: * * ```javascript * const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction, {channels: [1, 2, 3]}); * ``` * * In this case, the `listeners` variable contains an array of 3 [`Listener`](Listener) objects. * * The code above will add a listener for the `"noteon"` event and call `someFunction` when the * event is triggered on MIDI channels `1`, `2` or `3`. * * Note that, when adding events to channels, it is the [`InputChannel`](InputChannel) instance * that actually gets a listener added and not the `Input` instance. * * Note: if you want to add a listener to a single MIDI channel you should probably do so directly * on the [`InputChannel`](InputChannel) object itself. * * There are 8 families of events you can listen to: * * 1. **MIDI System Common** Events (input-wide) * * * [`songposition`]{@link Input#event:songposition} * * [`songselect`]{@link Input#event:songselect} * * [`sysex`]{@link Input#event:sysex} * * [`timecode`]{@link Input#event:timecode} * * [`tunerequest`]{@link Input#event:tunerequest} * * 2. **MIDI System Real-Time** Events (input-wide) * * * [`clock`]{@link Input#event:clock} * * [`start`]{@link Input#event:start} * * [`continue`]{@link Input#event:continue} * * [`stop`]{@link Input#event:stop} * * [`activesensing`]{@link Input#event:activesensing} * * [`reset`]{@link Input#event:reset} * * 3. **State Change** Events (input-wide) * * * [`opened`]{@link Input#event:opened} * * [`closed`]{@link Input#event:closed} * * [`disconnected`]{@link Input#event:disconnected} * * 4. **Catch-All** Events (input-wide) * * * [`midimessage`]{@link Input#event:midimessage} * * [`unknownmidimessage`]{@link Input#event:unknownmidimessage} * * 5. **Channel Voice** Events (channel-specific) * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * 6. **Channel Mode** Events (channel-specific) * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * 7. **NRPN** Events (channel-specific) * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * 8. **RPN** Events (channel-specific) * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of * such integers representing the MIDI channel(s) to listen on. This parameter is ignored for * input-wide events. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @returns {Listener|Listener[]} An array of all [`Listener`](Listener) objects that were * created. */ addOneTimeListener( e: Symbol | T, listener: InputEventMap[T], options?: { "arguments"?: any[]; "channels"?: number | number[]; "context"?: any; "duration"?: number; "prepend"?: boolean; } ): Listener | Listener[]; /** * Closes the input. When an input is closed, it cannot be used to listen to MIDI messages until * the input is opened again by calling [`Input.open()`](Input#open). * * **Note**: if what you want to do is stop events from being dispatched, you should use * [`eventsSuspended`](#eventsSuspended) instead. * * @returns {Promise} The promise is fulfilled with the `Input` object */ close(): Promise; /** * Destroys the `Input` by removing all listeners, emptying the [`channels`](#channels) array and * unlinking the MIDI subsystem. This is mostly for internal use. * * @returns {Promise} */ destroy(): Promise; /** * Checks whether the specified [`Forwarder`](Forwarder) object has already been attached to this * input. * * @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to check for (the * [`Forwarder`](Forwarder) object is returned when calling [`addForwarder()`](#addForwarder). * @returns {boolean} */ hasForwarder(forwarder: Forwarder): boolean; /** * Checks if the specified event type is already defined to trigger the specified callback * function. For channel-specific events, the function will return `true` only if all channels * have the listener defined. * * @param event {string|Symbol} The type of the event. * * @param listener {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of such * integers representing the MIDI channel(s) to check. This parameter is ignored for input-wide * events. * * @returns {boolean} Boolean value indicating whether or not the `Input` or `InputChannel` * already has this listener defined. */ hasListener( e: Symbol | T, listener: InputEventMap[T], options?: { "channels"?: number | number[]; } ): boolean; /** * Opens the input for usage. This is usually unnecessary as the port is opened automatically when * WebMidi is enabled. * * @returns {Promise} The promise is fulfilled with the `Input` object. */ open(): Promise; /** * Removes the specified [`Forwarder`](Forwarder) object from the input. * * @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to remove (the * [`Forwarder`](Forwarder) object is returned when calling `addForwarder()`. */ removeForwarder(forwarder: Forwarder): void; /** * Removes the specified listener for the specified event. If no listener is specified, all * listeners for the specified event will be removed. If no event is specified, all listeners for * the `Input` as well as all listeners for all [`InputChannel`]{@link InputChannel} objects will * be removed. * * By default, channel-specific listeners will be removed from all channels unless the * `options.channel` narrows it down. * * @param [type] {string} The type of the event. * * @param [listener] {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of * such integers representing the MIDI channel(s) to match. This parameter is ignored for * input-wide events. * * @param {*} [options.context] Only remove the listeners that have this exact context. * * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener( type?: Symbol | T, listener?: InputEventMap[T], options?: { "channels"?: number | number[]; "context"?: any; "remaining"?: number; } ): void; /** * Input port's connection state: `pending`, `open` or `closed`. * * @type {WebMidiApi.MIDIPortConnectionState} * @readonly */ get connection(): WebMidiApi.MIDIPortConnectionState; /** * ID string of the MIDI port. The ID is host-specific. Do not expect the same ID on different * platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for * the same port. * * @type {string} * @readonly */ get id(): string; /** * Name of the manufacturer of the device that makes this input port available. * * @type {string} * @readonly */ get manufacturer(): string; /** * Name of the MIDI input. * * @type {string} * @readonly */ get name(): string; /** * An integer to offset the reported octave of incoming notes. By default, middle C (MIDI note * number 60) is placed on the 4th octave (C4). * * If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If * `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. * * Note that this value is combined with the global offset value defined in the * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) property (if any). * * @type {number} * * @since 3.0 */ set octaveOffset(arg: number); get octaveOffset(): number; /** * State of the input port: `connected` or `disconnected`. * * @type {WebMidiApi.MIDIPortDeviceState} * @readonly */ get state(): WebMidiApi.MIDIPortDeviceState; /** * The port type. In the case of the `Input` object, this is always: `input`. * * @type {WebMidiApi.MIDIPortType} * @readonly */ get type(): WebMidiApi.MIDIPortType; } /** * The `InputChannel` class represents a single MIDI input channel (1-16) from a single input * device. This object is derived from the host's MIDI subsystem and should not be instantiated * directly. * * All 16 `InputChannel` objects can be found inside the input's [`channels`](Input#channels) * property. * * @fires InputChannel#midimessage * @fires InputChannel#unknownmessage * * @fires InputChannel#noteoff * @fires InputChannel#noteon * @fires InputChannel#keyaftertouch * @fires InputChannel#programchange * @fires InputChannel#channelaftertouch * @fires InputChannel#pitchbend * * @fires InputChannel#allnotesoff * @fires InputChannel#allsoundoff * @fires InputChannel#localcontrol * @fires InputChannel#monomode * @fires InputChannel#omnimode * @fires InputChannel#resetallcontrollers * * @fires InputChannel#event:nrpn * @fires InputChannel#event:nrpn-dataentrycoarse * @fires InputChannel#event:nrpn-dataentryfine * @fires InputChannel#event:nrpn-dataincrement * @fires InputChannel#event:nrpn-datadecrement * @fires InputChannel#event:rpn * @fires InputChannel#event:rpn-dataentrycoarse * @fires InputChannel#event:rpn-dataentryfine * @fires InputChannel#event:rpn-dataincrement * @fires InputChannel#event:rpn-datadecrement * * @fires InputChannel#controlchange * @fires InputChannel#event:controlchange-controllerxxx * @fires InputChannel#event:controlchange-bankselectcoarse * @fires InputChannel#event:controlchange-modulationwheelcoarse * @fires InputChannel#event:controlchange-breathcontrollercoarse * @fires InputChannel#event:controlchange-footcontrollercoarse * @fires InputChannel#event:controlchange-portamentotimecoarse * @fires InputChannel#event:controlchange-dataentrycoarse * @fires InputChannel#event:controlchange-volumecoarse * @fires InputChannel#event:controlchange-balancecoarse * @fires InputChannel#event:controlchange-pancoarse * @fires InputChannel#event:controlchange-expressioncoarse * @fires InputChannel#event:controlchange-effectcontrol1coarse * @fires InputChannel#event:controlchange-effectcontrol2coarse * @fires InputChannel#event:controlchange-generalpurposecontroller1 * @fires InputChannel#event:controlchange-generalpurposecontroller2 * @fires InputChannel#event:controlchange-generalpurposecontroller3 * @fires InputChannel#event:controlchange-generalpurposecontroller4 * @fires InputChannel#event:controlchange-bankselectfine * @fires InputChannel#event:controlchange-modulationwheelfine * @fires InputChannel#event:controlchange-breathcontrollerfine * @fires InputChannel#event:controlchange-footcontrollerfine * @fires InputChannel#event:controlchange-portamentotimefine * @fires InputChannel#event:controlchange-dataentryfine * @fires InputChannel#event:controlchange-channelvolumefine * @fires InputChannel#event:controlchange-balancefine * @fires InputChannel#event:controlchange-panfine * @fires InputChannel#event:controlchange-expressionfine * @fires InputChannel#event:controlchange-effectcontrol1fine * @fires InputChannel#event:controlchange-effectcontrol2fine * @fires InputChannel#event:controlchange-damperpedal * @fires InputChannel#event:controlchange-portamento * @fires InputChannel#event:controlchange-sostenuto * @fires InputChannel#event:controlchange-softpedal * @fires InputChannel#event:controlchange-legatopedal * @fires InputChannel#event:controlchange-hold2 * @fires InputChannel#event:controlchange-soundvariation * @fires InputChannel#event:controlchange-resonance * @fires InputChannel#event:controlchange-releasetime * @fires InputChannel#event:controlchange-attacktime * @fires InputChannel#event:controlchange-brightness * @fires InputChannel#event:controlchange-decaytime * @fires InputChannel#event:controlchange-vibratorate * @fires InputChannel#event:controlchange-vibratodepth * @fires InputChannel#event:controlchange-vibratodelay * @fires InputChannel#event:controlchange-generalpurposecontroller5 * @fires InputChannel#event:controlchange-generalpurposecontroller6 * @fires InputChannel#event:controlchange-generalpurposecontroller7 * @fires InputChannel#event:controlchange-generalpurposecontroller8 * @fires InputChannel#event:controlchange-portamentocontrol * @fires InputChannel#event:controlchange-highresolutionvelocityprefix * @fires InputChannel#event:controlchange-effect1depth * @fires InputChannel#event:controlchange-effect2depth * @fires InputChannel#event:controlchange-effect3depth * @fires InputChannel#event:controlchange-effect4depth * @fires InputChannel#event:controlchange-effect5depth * @fires InputChannel#event:controlchange-dataincrement * @fires InputChannel#event:controlchange-datadecrement * @fires InputChannel#event:controlchange-nonregisteredparameterfine * @fires InputChannel#event:controlchange-nonregisteredparametercoarse * @fires InputChannel#event:controlchange-registeredparameterfine * @fires InputChannel#event:controlchange-registeredparametercoarse * @fires InputChannel#event:controlchange-allsoundoff * @fires InputChannel#event:controlchange-resetallcontrollers * @fires InputChannel#event:controlchange-localcontrol * @fires InputChannel#event:controlchange-allnotesoff * @fires InputChannel#event:controlchange-omnimodeoff * @fires InputChannel#event:controlchange-omnimodeon * @fires InputChannel#event:controlchange-monomodeon * @fires InputChannel#event:controlchange-polymodeon * @fires InputChannel#event: * * @extends EventEmitter * @license Apache-2.0 * @since 3.0.0 */ export class InputChannel extends EventEmitter { /** * Creates an `InputChannel` object. * * @param {Input} input The [`Input`](Input) object this channel belongs to. * @param {number} number The channel's MIDI number (1-16). */ constructor(input: Input, number: number); private _dispatchParameterNumberEvent; private _input; private _isRpnOrNrpnController; private _number; private _octaveOffset; private _parseChannelModeMessage; private _parseEventForParameterNumber; private _parseEventForStandardMessages; private _processMidiMessageEvent; private _nrpnBuffer; private _rpnBuffer; /** * Contains the current playing state of all MIDI notes of this channel (0-127). The state is * `true` for a currently playing note and `false` otherwise. * @type {boolean[]} */ notesState: boolean[]; /** * Indicates whether events for **Registered Parameter Number** and **Non-Registered Parameter * Number** should be dispatched. RPNs and NRPNs are composed of a sequence of specific * **control change** messages. When a valid sequence of such control change messages is * received, an [`rpn`](#event-rpn) or [`nrpn`](#event-nrpn) event will fire. * * If an invalid or out-of-order **control change** message is received, it will fall through * the collector logic and all buffered **control change** messages will be discarded as * incomplete. * * @type {boolean} */ parameterNumberEventsEnabled: boolean; /** * Adds an event listener that will trigger a function callback when the specified event is * dispatched. * * Here are the events you can listen to: * * **Channel Voice** Events * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * **Channel Mode** Events * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * **NRPN** Events * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * **RPN** Events * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener} The listener object that was created */ addListener( e: Symbol | T, listener: InputChannelEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; "remaining"?: number; } ): Listener; /** * Adds a one-time event listener that will trigger a function callback when the specified event * is dispatched. * * Here are the events you can listen to: * * **Channel Voice** Events * * * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch} * * [`controlchange`]{@link InputChannel#event:controlchange} * * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0} * * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1} * * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2} * * (...) * * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127} * * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch} * * [`noteoff`]{@link InputChannel#event:noteoff} * * [`noteon`]{@link InputChannel#event:noteon} * * [`pitchbend`]{@link InputChannel#event:pitchbend} * * [`programchange`]{@link InputChannel#event:programchange} * * Note: you can listen for a specific control change message by using an event name like this: * `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, * etc. * * **Channel Mode** Events * * * [`allnotesoff`]{@link InputChannel#event:allnotesoff} * * [`allsoundoff`]{@link InputChannel#event:allsoundoff} * * [`localcontrol`]{@link InputChannel#event:localcontrol} * * [`monomode`]{@link InputChannel#event:monomode} * * [`omnimode`]{@link InputChannel#event:omnimode} * * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers} * * **NRPN** Events * * * [`nrpn`]{@link InputChannel#event:nrpn} * * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse} * * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine} * * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement} * * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement} * * **RPN** Events * * * [`rpn`]{@link InputChannel#event:rpn} * * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse} * * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine} * * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement} * * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement} * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener} The listener object that was created */ addOneTimeListener( e: Symbol | T, listener: InputChannelEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; } ): Listener; /** * Destroys the `InputChannel` by removing all listeners and severing the link with the MIDI * subsystem's input. */ destroy(): void; /** * Returns the playing status of the specified note (`true` if the note is currently playing, * `false` if it is not). The `note` parameter can be an unsigned integer (0-127), a note * identifier (`"C4"`, `"G#5"`, etc.) or a [`Note`]{@link Note} object. * * IF the note is specified using an integer (0-127), no octave offset will be applied. * * @param {number|string|Note} note The note to get the state for. The * [`octaveOffset`](#octaveOffset) (channel, input and global) will be factored in for note * identifiers and [`Note`]{@link Note} objects. * @returns {boolean} * @since version 3.0.0 */ getNoteState(note: number | string | Note): boolean; /** * Checks if the specified event type is already defined to trigger the specified callback * function. * * @param event {string|Symbol} The type of the event. * * @param listener {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @returns {boolean} Boolean value indicating whether or not the `Input` or `InputChannel` * already has this listener defined. */ hasListener( e: Symbol | T, listener: InputChannelEventMap[T] ): boolean; /** * Removes the specified listener for the specified event. If no listener is specified, all * listeners for the specified event will be removed. If no event is specified, all listeners * will be removed. * * @param [type] {string} The type of the event. * * @param [listener] {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @param {*} [options.context] Only remove the listeners that have this exact context. * * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener( type?: Symbol | T, listener?: InputChannelEventMap[T], options?: { "channels"?: number | number[]; "context"?: any; "remaining"?: number; } ): void; /** * The [`Input`](Input) this channel belongs to. * @type {Input} * @since 3.0 */ get input(): Input; /** * This channel's MIDI number (1-16). * @type {number} * @since 3.0 */ get number(): number; /** * An integer to offset the reported octave of incoming note-specific messages (`noteon`, * `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th * octave (C4). * * If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If * `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. * * Note that this value is combined with the global offset value defined by * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) object and with the value defined on the parent * input object with [`Input.octaveOffset`](Input#octaveOffset). * * @type {number} * * @since 3.0 */ set octaveOffset(arg: number); get octaveOffset(): number; } /** * The `Message` class represents a single MIDI message. It has several properties that make it * easy to make sense of the binary data it contains. * * @license Apache-2.0 * @since 3.0.0 */ export class Message { /** * Creates a new `Message` object from raw MIDI data. * * @param {Uint8Array} data The raw data of the MIDI message as a * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * of integers between `0` and `255`. */ constructor(data: Uint8Array); /** * The MIDI channel number (`1` - `16`) that the message is targeting. This is only for * channel-specific messages. For system messages, this will be left `undefined`. * * @type {number} * @readonly */ channel: number; /** * An integer identifying the MIDI command. For channel-specific messages, the value is 4-bit * and will be between `8` and `14`. For system messages, the value will be between `240` and * `255`. * * @type {number} * @readonly */ command: number; /** * An array containing all the bytes of the MIDI message. Each byte is an integer between `0` * and `255`. * * @type {number[]} * @readonly */ data: number[]; /** * An array of the the data byte(s) of the MIDI message (as opposed to the status byte). When * the message is a system exclusive message (sysex), `dataBytes` explicitly excludes the * manufacturer ID and the sysex end byte so only the actual data is included. * * @type {number[]} * @readonly */ dataBytes: number[]; /** * A boolean indicating whether the MIDI message is a channel-specific message. * * @type {boolean} * @readonly */ isChannelMessage: boolean; /** * A boolean indicating whether the MIDI message is a system message (not specific to a * channel). * * @type {boolean} * @readonly */ isSystemMessage: boolean; /** * When the message is a system exclusive message (sysex), this property contains an array with * either 1 or 3 entries that identify the manufacturer targeted by the message. * * To know how to translate these entries into manufacturer names, check out the official list: * https://www.midi.org/specifications-old/item/manufacturer-id-numbers * * @type {number[]} * @readonly */ manufacturerId: number[]; /** * A * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * containing the bytes of the MIDI message. Each byte is an integer between `0` and `255`. * * @type {Uint8Array} * @readonly */ rawData: Uint8Array; /** * A * [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) * of the data byte(s) of the MIDI message. When the message is a system exclusive message * (sysex), `rawDataBytes` explicitly excludes the manufacturer ID and the sysex end byte so * only the actual data is included. * * @type {Uint8Array} * @readonly */ rawDataBytes: Uint8Array; /** * The MIDI status byte of the message as an integer between `0` and `255`. * * @type {number} * @readonly */ statusByte: number; /** * The type of message as a string (`"noteon"`, `"controlchange"`, `"sysex"`, etc.) * * @type {string} * @readonly */ type: string; } /** * The `Note` class represents a single musical note such as `"D3"`, `"G#4"`, `"F-1"`, `"Gb7"`, etc. * * `Note` objects can be played back on a single channel by calling * [`OutputChannel.playNote()`]{@link OutputChannel#playNote} or, on multiple channels of the same * output, by calling [`Output.playNote()`]{@link Output#playNote}. * * The note has [`attack`](#attack) and [`release`](#release) velocities set at `0.5` by default. * These can be changed by passing in the appropriate option. It is also possible to set a * system-wide default for attack and release velocities by using the * [`WebMidi.defaults`](WebMidi#defaults) property. * * If you prefer to work with raw MIDI values (`0` to `127`), you can use [`rawAttack`](#rawAttack) and * [`rawRelease`](#rawRelease) to both get and set the values. * * The note may have a [`duration`](#duration). If it does, playback will be automatically stopped * when the duration has elapsed by sending a `"noteoff"` event. By default, the duration is set to * `Infinity`. In this case, it will never stop playing unless explicitly stopped by calling a * method such as [`OutputChannel.stopNote()`]{@link OutputChannel#stopNote}, * [`Output.stopNote()`]{@link Output#stopNote} or similar. * * @license Apache-2.0 * @since 3.0.0 */ export class Note { private _accidental: string; private _attack: number; private _duration: number; private _name: string; private _octave: number; private _release: number; /** * Creates a `Note` object. * * @param value {string|number} The value used to create the note. If an identifier string is used, * it must start with the note letter, optionally followed by an accidental and followed by the * octave number (`"C3"`, `"G#4"`, `"F-1"`, `"Db7"`, etc.). If a number is used, it must be an * integer between 0 and 127. In this case, middle C is considered to be C4 (note number 60). * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should be * explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @throws {Error} Invalid note identifier * @throws {RangeError} Invalid name value * @throws {RangeError} Invalid accidental value * @throws {RangeError} Invalid octave value * @throws {RangeError} Invalid duration value * @throws {RangeError} Invalid attack value * @throws {RangeError} Invalid release value */ constructor(value: string | number, options?: { duration?: number; attack?: number; release?: number; rawAttack?: number; rawRelease?: number; }); /** * Returns a MIDI note number offset by octave and/or semitone. If the calculated value is less * than 0, 0 will be returned. If the calculated value is more than 127, 127 will be returned. If * an invalid value is supplied, 0 will be used. * * @param [octaveOffset] {number} An integer to offset the note number by octave. * @param [semitoneOffset] {number} An integer to offset the note number by semitone. * @returns {number} An integer between 0 and 127 */ getOffsetNumber(octaveOffset?: number, semitoneOffset?: number): number; /** * The accidental (#, ##, b or bb) of the note. * @type {string} * @since 3.0.0 */ get accidental(): string; set accidental(arg: string); /** * The attack velocity of the note as a float between 0 and 1. * @type {number} * @since 3.0.0 */ set attack(arg: number); get attack(): number; /** * The duration of the note as a positive decimal number representing the number of milliseconds * that the note should play for. * * @type {number} * @since 3.0.0 */ set duration(arg: number); get duration(): number; /** * The name, optional accidental and octave of the note, as a string. * @type {string} * @since 3.0.0 */ set identifier(arg: string); get identifier(): string; /** * The name (letter) of the note. If you need the full name with octave and accidental, you can * use the [`identifier`]{@link Note#identifier} property instead. * @type {string} * @since 3.0.0 */ get name(): string; set name(arg: string); /** * The MIDI number of the note (`0` - `127`). This number is derived from the note identifier * using C4 as a reference for middle C. * * @type {number} * @readonly * @since 3.0.0 */ get number(): number; /** * The octave of the note. * @type {number} * @since 3.0.0 */ get octave(): number; set octave(arg: number); /** * The attack velocity of the note as a positive integer between 0 and 127. * @type {number} * @since 3.0.0 */ get rawAttack(): number; set rawAttack(arg: number); /** * The release velocity of the note as a positive integer between 0 and 127. * @type {number} * @since 3.0.0 */ get rawRelease(): number; set rawRelease(arg: number); /** * The release velocity of the note as an integer between 0 and 1. * @type {number} * @since 3.0.0 */ set release(arg: number); get release(): number; } /** * The `Output` class represents a single MIDI output port (not to be confused with a MIDI channel). * A port is made available by a MIDI device. A MIDI device can advertise several input and output * ports. Each port has 16 MIDI channels which can be accessed via the [`channels`](#channels) * property. * * The `Output` object is automatically instantiated by the library according to the host's MIDI * subsystem and should not be directly instantiated. * * You can access all available `Output` objects by referring to the * [`WebMidi.outputs`](WebMidi#outputs) array or by using methods such as * [`WebMidi.getOutputByName()`](WebMidi#getOutputByName) or * [`WebMidi.getOutputById()`](WebMidi#getOutputById). * * @fires Output#opened * @fires Output#disconnected * @fires Output#closed * * @extends EventEmitter * @license Apache-2.0 */ export class Output extends EventEmitter { /** * Creates an `Output` object. * * @param {WebMidiApi.MIDIOutput} midiOutput [`MIDIOutput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIOutput) * object as provided by the MIDI subsystem. */ constructor(midiOutput: WebMidiApi.MIDIOutput); private _midiOutput; private _octaveOffset; private _onStateChange; /** * Array containing the 16 [`OutputChannel`]{@link OutputChannel} objects available provided by * this `Output`. The channels are numbered 1 through 16. * * @type {OutputChannel[]} */ channels: OutputChannel[]; /** * Adds an event listener that will trigger a function callback when the specified event is * dispatched. * * Here are the events you can listen to: closed, disconnected, open. * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener} The listener object that was created */ addListener( e: Symbol | T, listener: PortEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; "remaining"?: number; } ): Listener; /** * Adds a one-time event listener that will trigger a function callback when the specified event * is dispatched. * * Here are the events you can listen to: closed, disconnected, open. * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @returns {Listener} The listener object that was created */ addOneTimeListener( e: Symbol | T, listener: PortEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; } ): Listener; /** * Clears all messages that have been queued but not yet delivered. * * **Warning**: this method has been defined in the specification but has not been implemented * yet. As soon as browsers implement it, it will work. * * You can check out the current status of this feature for Chromium (Chrome) here: * https://bugs.chromium.org/p/chromium/issues/detail?id=471798 * * @returns {Output} Returns the `Output` object so methods can be chained. */ clear(): Output; /** * Closes the output connection. When an output is closed, it cannot be used to send MIDI messages * until the output is opened again by calling [`open()`]{@link #open}. You can check * the connection status by looking at the [`connection`]{@link #connection} property. * * @returns {Promise} */ close(): Promise; /** * Destroys the `Output`. All listeners are removed, all channels are destroyed and the MIDI * subsystem is unlinked. * @returns {Promise} */ destroy(): Promise; /** * Checks if the specified event type is already defined to trigger the specified callback * function. * * @param event {string|Symbol} The type of the event. * * @param listener {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @returns {boolean} Boolean value indicating whether or not the `Input` or `InputChannel` * already has this listener defined. */ hasListener( e: Symbol | T, listener: PortEventMap[T] ): boolean; /** * Opens the output for usage. When the library is enabled, all ports are automatically opened. * This method is only useful for ports that have been manually closed. * * @returns {Promise} The promise is fulfilled with the `Output` object. */ open(): Promise; /** * Plays a note or an array of notes on one or more channels of this output. If you intend to play * notes on a single channel, you should probably use * [`OutputChannel.playNote()`](OutputChannel#playNote) instead. * * The first parameter is the note to play. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`]{@link Note} object * * The `playNote()` method sends a **note on** MIDI message for all specified notes on all * specified channels. If no channel is specified, it will send to all channels. If a `duration` * is set in the `options` parameter or in the [`Note`]{@link Note} object's * [`duration`]{@link Note#duration} property, it will also schedule a **note off** message to end * the note after said duration. If no `duration` is set, the note will simply play until a * matching **note off** message is sent with [`stopNote()`]{@link #stopNote}. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the durations and velocities defined in the * [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options` * parameter. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. When using a note identifier, * octave range must be between -1 and 9. The lowest note is C-1 (MIDI note number `0`) and the * highest note is G9 (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.duration=undefined] The number of milliseconds after which a * **note off** message will be scheduled. If left undefined, only a **note on** message is sent. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity * value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between * `0` and `127`). This has priority over the `attack` property. An invalid velocity value will * silently trigger the default of 64. * * @param {number} [options.release=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid * velocity value will silently trigger the default of `0.5`. This is only used with the * **note off** event triggered when `options.duration` is set. * * @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0` * and `127`). This has priority over the `release` property. An invalid velocity value will * silently trigger the default of 64. This is only used with the **note off** event triggered * when `options.duration` is set. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ playNote(note: number | string | Note | number[] | string[] | Note[], options?: { channels?: number | number[]; duration?: number; attack?: number; rawAttack?: number; release?: number; rawRelease?: number; time?: number | string; }): Output; /** * Removes the specified listener for the specified event. If no listener is specified, all * listeners for the specified event will be removed. * * @param [type] {string} The type of the event. * * @param [listener] {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @param {*} [options.context] Only remove the listeners that have this exact context. * * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener( type?: Symbol | T, listener?: PortEventMap[T], options?: { "context"?: any; "remaining"?: number; } ): void; /** * Sends a MIDI message on the MIDI output port. If no time is specified, the message will be * sent immediately. The message should be an array of 8 bit unsigned integers (0-225), a * [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array} * object or a [`Message`](Message) object. * * It is usually not necessary to use this method directly as you can use one of the simpler * helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), * [`sendControlChange()`](#sendControlChange), etc. * * Details on the format of MIDI messages are available in the summary of * [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message} * from the MIDI Manufacturers Association. * * @param message {number[]|Uint8Array|Message} An array of 8bit unsigned integers, a `Uint8Array` * object (not available in Node.js) containing the message bytes or a `Message` object. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The first byte (status) must be an integer between 128 and 255. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @license Apache-2.0 */ send(message: number[] | Uint8Array | Message, options?: { time?: number | string; }, legacy?: number): Output; /** * Sends a MIDI [**system exclusive**]{@link * https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages} * (*sysex*) message. There are two categories of system exclusive messages: manufacturer-specific * messages and universal messages. Universal messages are further divided into three subtypes: * * * Universal non-commercial (for research and testing): `0x7D` * * Universal non-realtime: `0x7E` * * Universal realtime: `0x7F` * * The method's first parameter (`identification`) identifies the type of message. If the value of * `identification` is `0x7D` (125), `0x7E` (126) or `0x7F` (127), the message will be identified * as a **universal non-commercial**, **universal non-realtime** or **universal realtime** message * (respectively). * * If the `identification` value is an array or an integer between 0 and 124, it will be used to * identify the manufacturer targeted by the message. The *MIDI Manufacturers Association* * maintains a full list of * [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers). * * The `data` parameter should only contain the data of the message. When sending out the actual * MIDI message, WEBMIDI.js will automatically prepend the data with the **sysex byte** (`0xF0`) * and the identification byte(s). It will also automatically terminate the message with the * **sysex end byte** (`0xF7`). * * To use the `sendSysex()` method, system exclusive message support must have been enabled. To * do so, you must set the `sysex` option to `true` when calling * [`WebMidi.enable()`]{@link WebMidi#enable}: * * ```js * WebMidi.enable({sysex: true}) * .then(() => console.log("System exclusive messages are enabled"); * ``` * * ##### Examples of manufacturer-specific system exclusive messages * * If you want to send a sysex message to a Korg device connected to the first output, you would * use the following code: * * ```js * WebMidi.outputs[0].sendSysex(0x42, [0x1, 0x2, 0x3, 0x4, 0x5]); * ``` * In this case `0x42` is the ID of the manufacturer (Korg) and `[0x1, 0x2, 0x3, 0x4, 0x5]` is the * data being sent. * * The parameters can be specified using any number notation (decimal, hex, binary, etc.). * Therefore, the code above is equivalent to this code: * * ```js * WebMidi.outputs[0].sendSysex(66, [1, 2, 3, 4, 5]); * ``` * * Some manufacturers are identified using 3 bytes. In this case, you would use a 3-position array * as the first parameter. For example, to send the same sysex message to a * *Native Instruments* device: * * ```js * WebMidi.outputs[0].sendSysex([0x00, 0x21, 0x09], [0x1, 0x2, 0x3, 0x4, 0x5]); * ``` * * There is no limit for the length of the data array. However, it is generally suggested to keep * system exclusive messages to 64Kb or less. * * ##### Example of universal system exclusive message * * If you want to send a universal sysex message, simply assign the correct identification number * in the first parameter. Number `0x7D` (125) is for non-commercial, `0x7E` (126) is for * non-realtime and `0x7F` (127) is for realtime. * * So, for example, if you wanted to send an identity request non-realtime message (`0x7E`), you * could use the following: * * ```js * WebMidi.outputs[0].sendSysex(0x7E, [0x7F, 0x06, 0x01]); * ``` * * For more details on the format of universal messages, consult the list of * [universal sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages). * * @param {number|number[]} identification An unsigned integer or an array of three unsigned * integers between `0` and `127` that either identify the manufacturer or sets the message to be * a **universal non-commercial message** (`0x7D`), a **universal non-realtime message** (`0x7E`) * or a **universal realtime message** (`0x7F`). The *MIDI Manufacturers Association* maintains a * full list of * [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers). * * @param {number[]|Uint8Array} [data] A `Uint8Array` or an array of unsigned integers between `0` * and `127`. This is the data you wish to transfer. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {DOMException} Failed to execute 'send' on 'MIDIOutput': System exclusive message is * not allowed. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index x is greater * than 0xFF. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendSysex(identification: number | number[], data?: number[] | Uint8Array, options?: { time?: number | string; }): Output; /** * Sends a MIDI **timecode quarter frame** message. Please note that no processing is being done * on the data. It is up to the developer to format the data according to the * [MIDI Timecode](https://en.wikipedia.org/wiki/MIDI_timecode) format. * * @param value {number} The quarter frame message content (integer between 0 and 127). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendTimecodeQuarterFrame(value: number, options?: { time?: number | string; }): Output; /** * Sends a **song position** MIDI message. The value is expressed in MIDI beats (between `0` and * `16383`) which are 16th note. Position `0` is always the start of the song. * * @param {number} [value=0] The MIDI beat to cue to (integer between `0` and `16383`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendSongPosition(value?: number, options?: { time?: number | string; }): Output; /** * Sends a **song select** MIDI message. * * @param {number} [value=0] The number of the song to select (integer between `0` and `127`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws The song number must be between 0 and 127. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendSongSelect(value?: number, options?: { time?: number | string; }): Output; /** * Sends a MIDI **tune request** real-time message. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuneRequest(options?: { time?: number | string; }): Output; /** * Sends a MIDI **clock** real-time message. According to the standard, there are 24 MIDI clocks * for every quarter note. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendClock(options?: { time?: number | string; }): Output; /** * Sends a **start** real-time message. A MIDI Start message starts the playback of the current * song at beat 0. To start playback elsewhere in the song, use the * [`sendContinue()`]{@link #sendContinue} method. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendStart(options?: { time?: number | string; }): Output; /** * Sends a **continue** real-time message. This resumes song playback where it was previously * stopped or where it was last cued with a song position message. To start playback from the * start, use the [`sendStart()`]{@link Output#sendStart}` method. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendContinue(options?: { time?: number | string; }): Output; /** * Sends a **stop** real-time message. This tells the device connected to this output to stop * playback immediately (or at the scheduled time, if specified). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendStop(options?: { time?: number | string; }): Output; /** * Sends an **active sensing** real-time message. This tells the device connected to this port * that the connection is still good. Active sensing messages are often sent every 300 ms if there * was no other activity on the MIDI port. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendActiveSensing(options?: { time?: number | string; }): Output; /** * Sends a **reset** real-time message. This tells the device connected to this output that it * should reset itself to a default state. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendReset(options?: { time?: number | string; }): Output; /** * Sends a MIDI **key aftertouch** message to the specified channel(s) at the scheduled time. This * is a key-specific aftertouch. For a channel-wide aftertouch message, use * [`setChannelAftertouch()`]{@link #setChannelAftertouch}. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending * an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a * [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the * previous types. When using a note identifier, octave range must be between `-1` and `9`. The * lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number * `127`). * * @param [pressure=0.5] {number} The pressure level (between 0 and 1). An invalid pressure value * will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the * pressure can be defined by using an integer between 0 and 127. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendKeyAftertouch(note: number | Note | string | number[] | Note[] | string[], pressure?: number, options?: { channels?: number | number[]; rawValue?: boolean; time?: number | string; }): Output; /** * Sends a MIDI **control change** message to the specified channel(s) at the scheduled time. The * control change message to send can be specified numerically (0-127) or by using one of the * following common names: * * | Number | Name | * |--------|-------------------------------| * | 0 |`bankselectcoarse` | * | 1 |`modulationwheelcoarse` | * | 2 |`breathcontrollercoarse` | * | 4 |`footcontrollercoarse` | * | 5 |`portamentotimecoarse` | * | 6 |`dataentrycoarse` | * | 7 |`volumecoarse` | * | 8 |`balancecoarse` | * | 10 |`pancoarse` | * | 11 |`expressioncoarse` | * | 12 |`effectcontrol1coarse` | * | 13 |`effectcontrol2coarse` | * | 18 |`generalpurposeslider3` | * | 19 |`generalpurposeslider4` | * | 32 |`bankselectfine` | * | 33 |`modulationwheelfine` | * | 34 |`breathcontrollerfine` | * | 36 |`footcontrollerfine` | * | 37 |`portamentotimefine` | * | 38 |`dataentryfine` | * | 39 |`volumefine` | * | 40 |`balancefine` | * | 42 |`panfine` | * | 43 |`expressionfine` | * | 44 |`effectcontrol1fine` | * | 45 |`effectcontrol2fine` | * | 64 |`holdpedal` | * | 65 |`portamento` | * | 66 |`sustenutopedal` | * | 67 |`softpedal` | * | 68 |`legatopedal` | * | 69 |`hold2pedal` | * | 70 |`soundvariation` | * | 71 |`resonance` | * | 72 |`soundreleasetime` | * | 73 |`soundattacktime` | * | 74 |`brightness` | * | 75 |`soundcontrol6` | * | 76 |`soundcontrol7` | * | 77 |`soundcontrol8` | * | 78 |`soundcontrol9` | * | 79 |`soundcontrol10` | * | 80 |`generalpurposebutton1` | * | 81 |`generalpurposebutton2` | * | 82 |`generalpurposebutton3` | * | 83 |`generalpurposebutton4` | * | 91 |`reverblevel` | * | 92 |`tremololevel` | * | 93 |`choruslevel` | * | 94 |`celestelevel` | * | 95 |`phaserlevel` | * | 96 |`dataincrement` | * | 97 |`datadecrement` | * | 98 |`nonregisteredparametercoarse` | * | 99 |`nonregisteredparameterfine` | * | 100 |`registeredparametercoarse` | * | 101 |`registeredparameterfine` | * | 120 |`allsoundoff` | * | 121 |`resetallcontrollers` | * | 122 |`localcontrol` | * | 123 |`allnotesoff` | * | 124 |`omnimodeoff` | * | 125 |`omnimodeon` | * | 126 |`monomodeon` | * | 127 |`polymodeon` | * * Note: as you can see above, not all control change message have a matching name. This does not * mean you cannot use the others. It simply means you will need to use their number (`0` - `127`) * instead of their name. While you can still use them, numbers `120` to `127` are usually * reserved for *channel mode* messages. See [`sendChannelMode()`]{@link #sendChannelMode} method * for more info. * * To view a list of all available **control change** messages, please consult [Table 3 - Control * Change Messages](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * from the MIDI specification. * * @param controller {number|string} The MIDI controller name or number (0-127). * * @param [value=0] {number} The value to send (0-127). * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} Controller numbers must be between 0 and 127. * @throws {RangeError} Invalid controller name. * * @return {Output} Returns the `Output` object so methods can be chained. */ sendControlChange(controller: number | string, value?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a **pitch bend range** message to the specified channel(s) at the scheduled time so that * they adjust the range used by their pitch bend lever. The range is specified by using the * `semitones` and `cents` parameters. For example, setting the `semitones` parameter to `12` * means that the pitch bend range will be 12 semitones above and below the nominal pitch. * * @param {number} [semitones=0] The desired adjustment value in semitones (between `0` and `127`). * While nothing imposes that in the specification, it is very common for manufacturers to limit * the range to 2 octaves (-12 semitones to 12 semitones). * * @param {number} [cents=0] The desired adjustment value in cents (integer between `0` and * `127`). * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The msb value must be between 0 and 127. * @throws {RangeError} The lsb value must be between 0 and 127. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPitchBendRange(semitones?: number, cents?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets the specified MIDI registered parameter to the desired value. The value is defined with * up to two bytes of data (msb, lsb) that each can go from `0` to `127`. * * MIDI * [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * extend the original list of control change messages. The MIDI 1.0 specification lists only a * limited number of them: * * | Numbers | Function | * |--------------|--------------------------| * | (0x00, 0x00) | `pitchbendrange` | * | (0x00, 0x01) | `channelfinetuning` | * | (0x00, 0x02) | `channelcoarsetuning` | * | (0x00, 0x03) | `tuningprogram` | * | (0x00, 0x04) | `tuningbank` | * | (0x00, 0x05) | `modulationrange` | * | (0x3D, 0x00) | `azimuthangle` | * | (0x3D, 0x01) | `elevationangle` | * | (0x3D, 0x02) | `gain` | * | (0x3D, 0x03) | `distanceratio` | * | (0x3D, 0x04) | `maximumdistance` | * | (0x3D, 0x05) | `maximumdistancegain` | * | (0x3D, 0x06) | `referencedistanceratio` | * | (0x3D, 0x07) | `panspreadangle` | * | (0x3D, 0x08) | `rollangle` | * * Note that the `tuningprogram` and `tuningbank` parameters are part of the *MIDI Tuning * Standard*, which is not widely implemented. * * @param parameter {string|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the * registered parameter. * * @param [data=[]] {number|number[]} A single integer or an array of integers with a maximum * length of 2 specifying the desired data. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnValue(parameter: string | number[], data?: number | number[], options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a MIDI **channel aftertouch** message to the specified channel(s). For key-specific * aftertouch, you should instead use [`setKeyAftertouch()`]{@link #setKeyAftertouch}. * * @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure * value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, * the pressure can be defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * @since 3.0.0 */ sendChannelAftertouch(pressure?: number, options?: { channels?: number | number[]; rawValue?: boolean; time?: number | string; }): Output; /** * Sends a MIDI **pitch bend** message to the specified channel(s) at the scheduled time. * * The resulting bend is relative to the pitch bend range that has been defined. The range can be * set with [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch * bend range has been set to 12 semitones, using a bend value of `-1` will bend the note 1 octave * below its nominal value. * * @param {number|number[]} value The intensity of the bend (between `-1.0` and `1.0`). A value of * `0` means no bend. If an invalid value is specified, the nearest valid value will be used * instead. If the `rawValue` option is set to `true`, the intensity of the bend can be defined by * either using a single integer between `0` and `127` (MSB) or an array of two integers between * `0` and `127` representing, respectively, the MSB (most significant byte) and the LSB (least * significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value lower * than `64` bends downwards while a value higher than `64` bends upwards. The LSB is expressed * in cents (1/100 of a semitone). An LSB of `64` also means no bend. * * @param {object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered as a float between `-1.0` and `1.0` (default) or as raw integer between `0` and * 127` (or an array of 2 integers if using both MSB and LSB). * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPitchBend(value: number | number[], options?: { channels?: number | number[]; rawValue?: boolean; time?: number | string; }): Output; /** * Sends a MIDI **program change** message to the specified channel(s) at the scheduled time. * * @param {number} [program=0] The MIDI patch (program) number (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater * than 0xFF. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendProgramChange(program?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a **modulation depth range** message to the specified channel(s) so that they adjust the * depth of their modulation wheel's range. The range can be specified with the `semitones` * parameter, the `cents` parameter or by specifying both parameters at the same time. * * @param [semitones=0] {number} The desired adjustment value in semitones (integer between * 0 and 127). * * @param [cents=0] {number} The desired adjustment value in cents (integer between 0 and 127). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The msb value must be between 0 and 127 * @throws {RangeError} The lsb value must be between 0 and 127 * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendModulationRange(semitones?: number, cents?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a master tuning message to the specified channel(s). The value is decimal and must be * larger than `-65` semitones and smaller than `64` semitones. * * Because of the way the MIDI specification works, the decimal portion of the value will be * encoded with a resolution of 14bit. The integer portion must be between -64 and 63 * inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and * a **Master Fine Tuning** RPN messages. * * @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64) * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The value must be a decimal number between larger than -65 and smaller * than 64. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendMasterTuning(value?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning program (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The program value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuningProgram(value: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param {number} [value=0] The desired tuning bank (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The bank value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendTuningBank(value?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a MIDI **channel mode** message to the specified channel(s). The channel mode message to * send can be specified numerically or by using one of the following common names: * * | Type |Number| Shortcut Method | * | ---------------------|------|-------------------------------------------------------------- | * | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} | * | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} | * | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} | * | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} | * | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} | * | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} | * | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} | * | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} | * * Note: as you can see above, to make it easier, all channel mode messages also have a matching * helper method. * * It should also be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` * may require a value that's not zero. For that reason, the `value` parameter is optional and * defaults to 0. * * @param {number|string} command The numerical identifier of the channel mode message (integer * between 120-127) or its name as a string. * * @param {number} [value=0] The value to send (integer between 0-127). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * */ sendChannelMode(command: number | string, value?: number, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends an **all sound off** channel mode message. This will silence all sounds playing on that * channel but will not prevent new sounds from being triggered. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} * * @since 3.0.0 */ sendAllSoundOff(options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends an **all notes off** channel mode message. This will make all currently playing notes * fade out just as if their key had been released. This is different from the * [`turnSoundOff()`]{@link #turnSoundOff} method which mutes all sounds immediately. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} * * @since 3.0.0 */ sendAllNotesOff(options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a **reset all controllers** channel mode message. This resets all controllers, such as * the pitch bend, to their default value. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} */ sendResetAllControllers(options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets the polyphonic mode. In `poly` mode (usually the default), multiple notes can be played * and heard at the same time. In `mono` mode, only one note will be heard at once even if * multiple notes are being played. * * @param mode {string} The mode to use: `mono` or `poly`. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendPolyphonicMode(mode: string, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Turns local control on or off. Local control is usually enabled by default. If you disable it, * the instrument will no longer trigger its own sounds. It will only send the MIDI messages to * its out port. * * @param [state=false] {boolean} Whether to activate local control (`true`) or disable it * (`false`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendLocalControl(state?: boolean, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets OMNI mode to **on** or **off** for the specified channel(s). MIDI's OMNI mode causes the * instrument to respond to messages from all channels. * * It should be noted that support for OMNI mode is not as common as it used to be. * * @param [state] {boolean} Whether to activate OMNI mode (`true`) or not (`false`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * * @since 3.0.0 */ sendOmniMode(state?: boolean, options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sets a non-registered parameter to the specified value. The NRPN is selected by passing a * two-position array specifying the values of the two control bytes. The value is specified by * passing a single integer (most cases) or an array of two integers. * * NRPNs are not standardized in any way. Each manufacturer is free to implement them any way * they see fit. For example, according to the Roland GS specification, you can control the * **vibrato rate** using NRPN (`1`, `8`). Therefore, to set the **vibrato rate** value to `123` * you would use: * * ```js * WebMidi.outputs[0].sendNrpnValue([1, 8], 123); * ``` * * You probably want to should select a channel so the message is not sent to all channels. For * instance, to send to channel `1` of the first output port, you would use: * * ```js * WebMidi.outputs[0].sendNrpnValue([1, 8], 123, 1); * ``` * * In some rarer cases, you need to send two values with your NRPN messages. In such cases, you * would use a 2-position array. For example, for its **ClockBPM** parameter (`2`, `63`), Novation * uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the * value to send was `10`, you could use: * * ```js * WebMidi.outputs[0].sendNrpnValue([2, 63], [0, 10], 1); * ``` * * For further implementation details, refer to the manufacturer's documentation. * * @param parameter {number[]} A two-position array specifying the two control bytes (`0x63`, * `0x62`) that identify the non-registered parameter. * * @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2 * specifying the desired data. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The control value must be between 0 and 127. * @throws {RangeError} The msb value must be between 0 and 127 * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNrpnValue(parameter: number[], data?: number | number[], options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Increments the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this method: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnIncrement(parameter: string | number[], options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this method: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified parameter is not available. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendRpnDecrement(parameter: string | number[], options?: { channels?: number | number[]; time?: number | string; }): Output; /** * Sends a **note off** message for the specified MIDI note number on the specified channel(s). * The first parameter is the note to stop. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, * `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range * must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest * note is `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNoteOff(note: number | Note | string | number[] | Note[] | string[], options?: { channels?: number | number[]; release?: number; rawRelease?: number; time?: number | string; }): Output; /** * Sends a **note off** message for the specified MIDI note number on the specified channel(s). * The first parameter is the note to stop. It can be a single value or an array of the following * valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ stopNote(note: number | Note | string | number[] | Note[] | string[], options?: { channels?: number | number[]; release?: number; rawRelease?: number; time?: number | string; }): Output; /** * Sends a **note on** message for the specified MIDI note number on the specified channel(s). The * first parameter is the number. It can be a single value or an array of the following valid * values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]] * The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no * channel is specified, all channels will be used. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0` * and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ sendNoteOn(note: number | Note | string | number[] | Note[] | string[], options?: { channels?: number | number[]; attack?: number; rawAttack?: number; time?: number | string; }): Output; /** * Output port's connection state: `pending`, `open` or `closed`. * * @type {WebMidiApi.MIDIPortConnectionState} * @readonly */ get connection(): WebMidiApi.MIDIPortConnectionState; /** * ID string of the MIDI output. The ID is host-specific. Do not expect the same ID on different * platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for * the same port. * * @type {string} * @readonly */ get id(): string; /** * Name of the manufacturer of the device that makes this output port available. * * @type {string} * @readonly */ get manufacturer(): string; /** * Name of the MIDI output. * * @type {string} * @readonly */ get name(): string; /** * An integer to offset the octave of outgoing notes. By default, middle C (MIDI note number 60) * is placed on the 4th octave (C4). * * Note that this value is combined with the global offset value defined in * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) (if any). * * @type {number} * * @since 3.0 */ set octaveOffset(arg: number); get octaveOffset(): number; /** * State of the output port: `connected` or `disconnected`. * * @type {WebMidiApi.MIDIPortDeviceState} * @readonly */ get state(): WebMidiApi.MIDIPortDeviceState; /** * Type of the output port (it will always be: `output`). * * @type {WebMidiApi.MIDIPortType} * @readonly */ get type(): WebMidiApi.MIDIPortType; } /** * The `OutputChannel` class represents a single output MIDI channel. `OutputChannel` objects are * provided by an [`Output`](Output) port which, itself, is made available by a device. The * `OutputChannel` object is derived from the host's MIDI subsystem and should not be instantiated * directly. * * All 16 `OutputChannel` objects can be found inside the parent output's * [`channels`]{@link Output#channels} property. * * @param {Output} output The [`Output`](Output) this channel belongs to. * @param {number} number The MIDI channel number (`1` - `16`). * * @extends EventEmitter * @license Apache-2.0 * @since 3.0.0 */ export class OutputChannel extends EventEmitter { /** * Creates an `OutputChannel` object. * * @param {Output} output The [`Output`](Output) this channel belongs to. * @param {number} number The MIDI channel number (`1` - `16`). */ constructor(output: Output, number: number); private _output; private _number; private _octaveOffset; private destroy; /** * Sends a MIDI message on the MIDI output port. If no time is specified, the message will be * sent immediately. The message should be an array of 8-bit unsigned integers (`0` - `225`), * a * [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array} * object or a [`Message`](Message) object. * * It is usually not necessary to use this method directly as you can use one of the simpler * helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), * [`sendControlChange()`](#sendControlChange), etc. * * Details on the format of MIDI messages are available in the summary of * [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message} * from the MIDI Manufacturers Association. * * @param message {number[]|Uint8Array|Message} A `Message` object, an array of 8-bit unsigned * integers or a `Uint8Array` object (not available in Node.js) containing the message bytes. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The first byte (status) must be an integer between 128 and 255. * * @throws {RangeError} Data bytes must be integers between 0 and 255. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ send(message: number[] | Uint8Array | Message, options?: { time?: number | string; }): OutputChannel; /** * Sends a MIDI **key aftertouch** message at the scheduled time. This is a key-specific * aftertouch. For a channel-wide aftertouch message, use * [`sendChannelAftertouch()`]{@link #sendChannelAftertouch}. * * @param target {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending * an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a * [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the * previous types. When using a note identifier, octave range must be between `-1` and `9`. The * lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number * `127`). * * When using a note identifier, the octave value will be offset by the local * [`octaveOffset`](#octaveOffset) and by * [`Output.octaveOffset`](Output#octaveOffset) and [`WebMidi.octaveOffset`](WebMidi#octaveOffset) * (if those values are not `0`). When using a key number, `octaveOffset` values are ignored. * * @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure * value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, * the pressure is defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @return {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @throws RangeError Invalid key aftertouch value. */ sendKeyAftertouch(target: number | Note | string | number[] | Note[] | string[], pressure?: number, options?: { rawValue?: boolean; time?: number | string; }): OutputChannel; /** * Sends a MIDI **control change** message to the channel at the scheduled time. The control * change message to send can be specified numerically (`0` to `127`) or by using one of the * following common names: * * | Number | Name | * |--------|-------------------------------| * | 0 |`bankselectcoarse` | * | 1 |`modulationwheelcoarse` | * | 2 |`breathcontrollercoarse` | * | 4 |`footcontrollercoarse` | * | 5 |`portamentotimecoarse` | * | 6 |`dataentrycoarse` | * | 7 |`volumecoarse` | * | 8 |`balancecoarse` | * | 10 |`pancoarse` | * | 11 |`expressioncoarse` | * | 12 |`effectcontrol1coarse` | * | 13 |`effectcontrol2coarse` | * | 18 |`generalpurposeslider3` | * | 19 |`generalpurposeslider4` | * | 32 |`bankselectfine` | * | 33 |`modulationwheelfine` | * | 34 |`breathcontrollerfine` | * | 36 |`footcontrollerfine` | * | 37 |`portamentotimefine` | * | 38 |`dataentryfine` | * | 39 |`volumefine` | * | 40 |`balancefine` | * | 42 |`panfine` | * | 43 |`expressionfine` | * | 44 |`effectcontrol1fine` | * | 45 |`effectcontrol2fine` | * | 64 |`holdpedal` | * | 65 |`portamento` | * | 66 |`sustenutopedal` | * | 67 |`softpedal` | * | 68 |`legatopedal` | * | 69 |`hold2pedal` | * | 70 |`soundvariation` | * | 71 |`resonance` | * | 72 |`soundreleasetime` | * | 73 |`soundattacktime` | * | 74 |`brightness` | * | 75 |`soundcontrol6` | * | 76 |`soundcontrol7` | * | 77 |`soundcontrol8` | * | 78 |`soundcontrol9` | * | 79 |`soundcontrol10` | * | 80 |`generalpurposebutton1` | * | 81 |`generalpurposebutton2` | * | 82 |`generalpurposebutton3` | * | 83 |`generalpurposebutton4` | * | 91 |`reverblevel` | * | 92 |`tremololevel` | * | 93 |`choruslevel` | * | 94 |`celestelevel` | * | 95 |`phaserlevel` | * | 96 |`dataincrement` | * | 97 |`datadecrement` | * | 98 |`nonregisteredparametercoarse` | * | 99 |`nonregisteredparameterfine` | * | 100 |`registeredparametercoarse` | * | 101 |`registeredparameterfine` | * | 120 |`allsoundoff` | * | 121 |`resetallcontrollers` | * | 122 |`localcontrol` | * | 123 |`allnotesoff` | * | 124 |`omnimodeoff` | * | 125 |`omnimodeon` | * | 126 |`monomodeon` | * | 127 |`polymodeon` | * * As you can see above, not all control change message have a matching name. This does not mean * you cannot use the others. It simply means you will need to use their number * (`0` to `127`) instead of their name. While you can still use them, numbers `120` to `127` are * usually reserved for *channel mode* messages. See * [`sendChannelMode()`]{@link OutputChannel#sendChannelMode} method for more info. * * To view a detailed list of all available **control change** messages, please consult "Table 3 - * Control Change Messages" from the [MIDI Messages]( * https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2) * specification. * * **Note**: messages #0-31 (MSB) are paired with messages #32-63 (LSB). For example, message #1 * (`modulationwheelcoarse`) can be accompanied by a second control change message for * `modulationwheelfine` to achieve a greater level of precision. if you want to specify both MSB * and LSB for messages between `0` and `31`, you can do so by passing a 2-value array as the * second parameter. * * @param {number|string} controller The MIDI controller name or number (`0` - `127`). * * @param {number|number[]} value The value to send (0-127). You can also use a two-position array * for controllers 0 to 31. In this scenario, the first value will be sent as usual and the second * value will be sent to the matching LSB controller (which is obtained by adding 32 to the first * controller) * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} Controller numbers must be between 0 and 127. * @throws {RangeError} Invalid controller name. * @throws {TypeError} The value array must have a length of 2. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @license Apache-2.0 * @since 3.0.0 */ sendControlChange(controller: number | string, value: number | number[], options?: { time?: number | string; }): OutputChannel; private _selectNonRegisteredParameter; private _deselectRegisteredParameter; private _deselectNonRegisteredParameter; private _selectRegisteredParameter; private _setCurrentParameter; /** * Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this function: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified registered parameter is invalid. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnDecrement(parameter: string | number[], options?: { time?: number | string; }): OutputChannel; /** * Increments the specified MIDI registered parameter by 1. Here is the full list of parameter * names that can be used with this function: * * * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * * Tuning Program (0x00, 0x03): `"tuningprogram"` * * Tuning Bank (0x00, 0x04): `"tuningbank"` * * Modulation Range (0x00, 0x05): `"modulationrange"` * * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * * Elevation Angle (0x3D, 0x01): `"elevationangle"` * * Gain (0x3D, 0x02): `"gain"` * * Distance Ratio (0x3D, 0x03): `"distanceratio"` * * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * * Roll Angle (0x3D, 0x08): `"rollangle"` * * @param parameter {String|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (0x65, 0x64) that identify the registered * parameter. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws TypeError The specified registered parameter is invalid. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnIncrement(parameter: string | number[], options?: { time?: number | string; }): OutputChannel; /** * Plays a note or an array of notes on the channel. The first parameter is the note to play. It * can be a single value or an array of the following valid values: * * - A [`Note`]{@link Note} object * - A MIDI note number (integer between `0` and `127`) * - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * * The `playNote()` method sends a **note on** MIDI message for all specified notes. If a * `duration` is set in the `options` parameter or in the [`Note`]{@link Note} object's * [`duration`]{@link Note#duration} property, it will also schedule a **note off** message * to end the note after said duration. If no `duration` is set, the note will simply play until * a matching **note off** message is sent with [`stopNote()`]{@link OutputChannel#stopNote} or * [`sendNoteOff()`]{@link OutputChannel#sendNoteOff}. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the durations and velocities defined in the * [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options` * parameter. * * **Note**: per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, * `F-1`, `Db7`), a [`Note`]{@link Note} object or an array of the previous types. When using a * note identifier, the octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI * note number `0`) and the highest note is `G9` (MIDI note number `127`). * * @param {object} [options={}] * * @param {number} [options.duration] A positive decimal number larger than `0` representing the * number of milliseconds to wait before sending a **note off** message. If invalid or left * undefined, only a **note on** message will be sent. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity * value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between * `0` and `127`). This has priority over the `attack` property. An invalid velocity value will * silently trigger the default of 64. * * @param {number} [options.release=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid * velocity value will silently trigger the default of `0.5`. This is only used with the * **note off** event triggered when `options.duration` is set. * * @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0` * and `127`). This has priority over the `release` property. An invalid velocity value will * silently trigger the default of 64. This is only used with the **note off** event triggered * when `options.duration` is set. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ playNote(note: number | string | Note | number[] | string[] | Note[], options?: { duration?: number; attack?: number; rawAttack?: number; release?: number; rawRelease?: number; time?: number | string; }): OutputChannel; /** * Sends a **note off** message for the specified notes on the channel. The first parameter is the * note. It can be a single value or an array of the following valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`]{@link Note} object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the release velocity defined in the * [`Note`]{@link Note} objects has precedence over the one specified via the method's `options` * parameter. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. When using a note name, octave * range must be between -1 and 9. The lowest note is C-1 (MIDI note number 0) and the highest * note is G9 (MIDI note number 127). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNoteOff(note: number | string | Note | number[] | string[] | Note[], options?: { time?: number | string; release?: number; rawRelease?: number; }): OutputChannel; /** * Sends a **note off** message for the specified MIDI note number. The first parameter is the * note to stop. It can be a single value or an array of the following valid values: * * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * - A [`Note`](Note) object * * The execution of the **note off** command can be delayed by using the `time` property of the * `options` parameter. * * @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be * specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, * `Db7`) or an array of the previous types. When using a note identifier, octave range must be * between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is * `G9` (MIDI note number `127`). * * @param {Object} [options={}] * * @param {number} [options.release=0.5] The velocity at which to release the note * (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawRelease=64] The velocity at which to release the note * (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have * priority. An invalid velocity value will silently trigger the default of `64`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ stopNote(note: number | Note | string | number[] | Note[] | string[], options?: { release?: number; rawRelease?: number; time?: number | string; }): Output; /** * Sends a **note on** message for the specified note(s) on the channel. The first parameter is * the note. It can be a single value or an array of the following valid values: * * - A [`Note`]{@link Note} object * - A MIDI note number (integer between `0` and `127`) * - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) * * When passing a [`Note`]{@link Note}object or a note name, the `octaveOffset` will be applied. * This is not the case when using a note number. In this case, we assume you know exactly which * MIDI note number should be sent out. * * The execution of the **note on** command can be delayed by using the `time` property of the * `options` parameter. * * When using [`Note`]{@link Note} objects, the attack velocity defined in the * [`Note`]{@link Note} objects has precedence over the one specified via the method's `options` * parameter. Also, the `duration` is ignored. If you want to also send a **note off** message, * use the [`playNote()`]{@link #playNote} method instead. * * **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is * functionally equivalent to a **note off** message. * * @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be * specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a * [`Note`]{@link Note} object or an array of the previous types. * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `0.5`. * * @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0` * and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid * velocity value will silently trigger the default of `64`. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNoteOn(note: number | string | Note | number[] | string[] | Note[], options?: { time?: number | string; attack?: number; rawAttack?: number; }): OutputChannel; /** * Sends a MIDI **channel mode** message. The channel mode message to send can be specified * numerically or by using one of the following common names: * * | Type |Number| Shortcut Method | * | ---------------------|------|-------------------------------------------------------------- | * | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} | * | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} | * | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} | * | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} | * | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} | * | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} | * | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} | * | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} | * * **Note**: as you can see above, to make it easier, all channel mode messages also have a matching * helper method. * * It should be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` may * require a value that's not zero. For that reason, the `value` parameter is optional and * defaults to 0. * * @param {number|string} command The numerical identifier of the channel mode message (integer * between `120` and `127`) or its name as a string. * * @param {number} [value=0] The value to send (integer between `0` - `127`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendChannelMode(command: number | string, value?: number, options?: { time?: number | string; }): OutputChannel; /** * Sets OMNI mode to `"on"` or `"off"`. MIDI's OMNI mode causes the instrument to respond to * messages from all channels. * * It should be noted that support for OMNI mode is not as common as it used to be. * * @param [state=true] {boolean} Whether to activate OMNI mode (`true`) or not (`false`). * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Invalid channel mode message name. * @throws {RangeError} Channel mode controller numbers must be between 120 and 127. * @throws {RangeError} Value must be an integer between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendOmniMode(state?: boolean, options?: { time?: number | string; }): OutputChannel; /** * Sends a MIDI **channel aftertouch** message. For key-specific aftertouch, you should instead * use [`sendKeyAftertouch()`]{@link #sendKeyAftertouch}. * * @param [pressure] {number} The pressure level (between `0` and `1`). If the `rawValue` option * is set to `true`, the pressure can be defined by using an integer between `0` and `127`. * * @param {object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`. * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * * @throws RangeError Invalid channel aftertouch value. */ sendChannelAftertouch(pressure?: number, options?: { rawValue?: boolean; time?: number | string; }): OutputChannel; /** * Sends a **master tuning** message. The value is decimal and must be larger than -65 semitones * and smaller than 64 semitones. * * Because of the way the MIDI specification works, the decimal portion of the value will be * encoded with a resolution of 14bit. The integer portion must be between -64 and 63 * inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and * a **Master Fine Tuning** RPN messages. * * @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64) * * @param {object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The value must be a decimal number between larger than -65 and smaller * than 64. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendMasterTuning(value?: number, options?: { time?: number | string; }): OutputChannel; /** * Sends a **modulation depth range** message to adjust the depth of the modulation wheel's range. * The range can be specified with the `semitones` parameter, the `cents` parameter or by * specifying both parameters at the same time. * * @param {number} semitones The desired adjustment value in semitones (integer between 0 and * 127). * * @param {number} [cents=0] The desired adjustment value in cents (integer between 0 and 127). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendModulationRange(semitones: number, cents?: number, options?: { time?: number | string; }): OutputChannel; /** * Sets a non-registered parameter (NRPN) to the specified value. The NRPN is selected by passing * in a two-position array specifying the values of the two control bytes. The value is specified * by passing in a single integer (most cases) or an array of two integers. * * NRPNs are not standardized in any way. Each manufacturer is free to implement them any way * they see fit. For example, according to the Roland GS specification, you can control the * **vibrato rate** using NRPN (1, 8). Therefore, to set the **vibrato rate** value to **123** you * would use: * * ```js * WebMidi.outputs[0].channels[0].sendNrpnValue([1, 8], 123); * ``` * * In some rarer cases, you need to send two values with your NRPN messages. In such cases, you * would use a 2-position array. For example, for its **ClockBPM** parameter (2, 63), Novation * uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the * value to send was 10, you could use: * * ```js * WebMidi.outputs[0].channels[0].sendNrpnValue([2, 63], [0, 10]); * ``` * * For further implementation details, refer to the manufacturer's documentation. * * @param nrpn {number[]} A two-position array specifying the two control bytes (0x63, * 0x62) that identify the non-registered parameter. * * @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2 * specifying the desired data. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The control value must be between 0 and 127. * @throws {RangeError} The msb value must be between 0 and 127 * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendNrpnValue(nrpn: number[], data?: number | number[], options?: { time?: number | string; }): OutputChannel; /** * Sends a MIDI **pitch bend** message at the scheduled time. The resulting bend is relative to * the pitch bend range that has been defined. The range can be set with * [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch * bend range has been set to 12 semitones, using a bend value of -1 will bend the note 1 octave * below its nominal value. * * @param {number|number[]} [value] The intensity of the bend (between -1.0 and 1.0). A value of * zero means no bend. If the `rawValue` option is set to `true`, the intensity of the bend can be * defined by either using a single integer between 0 and 127 (MSB) or an array of two integers * between 0 and 127 representing, respectively, the MSB (most significant byte) and the LSB * (least significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value * lower than `64` bends downwards while a value higher than `64` bends upwards. The LSB is * expressed in cents (1/100 of a semitone). An LSB of `64` also means no bend. * * @param {Object} [options={}] * * @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be * considered as a float between -1.0 and 1.0 (default) or as raw integer between 0 and 127 (or * an array of 2 integers if using both MSB and LSB). * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPitchBend(value?: number | number[], options?: { rawValue?: boolean; time?: number | string; }): OutputChannel; /** * Sends a **pitch bend range** message at the scheduled time to adjust the range used by the * pitch bend lever. The range is specified by using the `semitones` and `cents` parameters. For * example, setting the `semitones` parameter to `12` means that the pitch bend range will be 12 * semitones above and below the nominal pitch. * * @param semitones {number} The desired adjustment value in semitones (between 0 and 127). While * nothing imposes that in the specification, it is very common for manufacturers to limit the * range to 2 octaves (-12 semitones to 12 semitones). * * @param [cents=0] {number} The desired adjustment value in cents (integer between 0-127). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The semitones value must be an integer between 0 and 127. * @throws {RangeError} The cents value must be an integer between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPitchBendRange(semitones: number, cents?: number, options?: { time?: number | string; }): OutputChannel; /** * Sends a MIDI **program change** message at the scheduled time. * * @param [program=1] {number} The MIDI patch (program) number (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater * than 0xFF. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. * */ sendProgramChange(program?: number, options?: { time?: number | string; }): OutputChannel; /** * Sets the specified MIDI registered parameter to the desired value. The value is defined with * up to two bytes of data (msb, lsb) that each can go from 0 to 127. * * MIDI * [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) * extend the original list of control change messages. The MIDI 1.0 specification lists only a * limited number of them: * * | Numbers | Function | * |--------------|--------------------------| * | (0x00, 0x00) | `pitchbendrange` | * | (0x00, 0x01) | `channelfinetuning` | * | (0x00, 0x02) | `channelcoarsetuning` | * | (0x00, 0x03) | `tuningprogram` | * | (0x00, 0x04) | `tuningbank` | * | (0x00, 0x05) | `modulationrange` | * | (0x3D, 0x00) | `azimuthangle` | * | (0x3D, 0x01) | `elevationangle` | * | (0x3D, 0x02) | `gain` | * | (0x3D, 0x03) | `distanceratio` | * | (0x3D, 0x04) | `maximumdistance` | * | (0x3D, 0x05) | `maximumdistancegain` | * | (0x3D, 0x06) | `referencedistanceratio` | * | (0x3D, 0x07) | `panspreadangle` | * | (0x3D, 0x08) | `rollangle` | * * Note that the **Tuning Program** and **Tuning Bank** parameters are part of the *MIDI Tuning * Standard*, which is not widely implemented. * * @param rpn {string|number[]} A string identifying the parameter's name (see above) or a * two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the * registered parameter. * * @param [data=[]] {number|number[]} An single integer or an array of integers with a maximum * length of 2 specifying the desired data. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendRpnValue(rpn: string | number[], data?: number | number[], options?: { time?: number | string; }): OutputChannel; /** * Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning bank (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The bank value must be between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendTuningBank(value: number, options?: { time?: number | string; }): OutputChannel; /** * Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the * *MIDI Tuning Standard*, which is not widely implemented. * * @param value {number} The desired tuning program (integer between `0` and `127`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @throws {RangeError} The program value must be between 0 and 127. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendTuningProgram(value: number, options?: { time?: number | string; }): OutputChannel; /** * Turns local control on or off. Local control is usually enabled by default. If you disable it, * the instrument will no longer trigger its own sounds. It will only send the MIDI messages to * its out port. * * @param [state=false] {boolean} Whether to activate local control (`true`) or disable it * (`false`). * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendLocalControl(state?: boolean, options?: { time?: number | string; }): OutputChannel; /** * Sends an **all notes off** channel mode message. This will make all currently playing notes * fade out just as if their key had been released. This is different from the * [`sendAllSoundOff()`]{@link #sendAllSoundOff} method which mutes all sounds immediately. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendAllNotesOff(options?: { time?: number | string; }): OutputChannel; /** * Sends an **all sound off** channel mode message. This will silence all sounds playing on that * channel but will not prevent new sounds from being triggered. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendAllSoundOff(options?: { time?: number | string; }): OutputChannel; /** * Sends a **reset all controllers** channel mode message. This resets all controllers, such as * the pitch bend, to their default value. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendResetAllControllers(options?: { time?: number | string; }): OutputChannel; /** * Sets the polyphonic mode. In `"poly"` mode (usually the default), multiple notes can be played * and heard at the same time. In `"mono"` mode, only one note will be heard at once even if * multiple notes are being played. * * @param {string} [mode=poly] The mode to use: `"mono"` or `"poly"`. * * @param {Object} [options={}] * * @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and * followed by a number, the message will be delayed by that many milliseconds. If the value is a * positive number * ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}), * the operation will be scheduled for that time. The current time can be retrieved with * [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the * operation will be carried out as soon as possible. * * @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained. */ sendPolyphonicMode(mode?: string, options?: { time?: number | string; }): OutputChannel; set octaveOffset(arg: number); /** * An integer to offset the reported octave of outgoing note-specific messages (`noteon`, * `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th * octave (C4). * * Note that this value is combined with the global offset value defined in * [`WebMidi.octaveOffset`](WebMidi#octaveOffset) and with the parent value defined in * [`Output.octaveOffset`]{@link Output#octaveOffset}. * * @type {number} * * @since 3.0 */ get octaveOffset(): number; /** * The parent [`Output`]{@link Output} this channel belongs to. * @type {Output} * @since 3.0 */ get output(): Output; /** * This channel's MIDI number (`1` - `16`). * @type {number} * @since 3.0 */ get number(): number; } /** * The `Utilities` class contains general-purpose utility methods. All methods are static and * should be called using the class name. For example: `Utilities.getNoteDetails("C4")`. * * @license Apache-2.0 * @since 3.0.0 */ export class Utilities { /** * Converts the `input` parameter to a valid [`Note`]{@link Note} object. The input usually is an * unsigned integer (0-127) or a note identifier (`"C4"`, `"G#5"`, etc.). If the input is a * [`Note`]{@link Note} object, it will be returned as is. * * If the input is a note number or identifier, it is possible to specify options by providing the * `options` parameter. * * @param [input] {number|string|Note} * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should * be explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only * used when the input value is a note identifier.** * * @returns {Note} * * @throws TypeError The input could not be parsed to a note * * @since version 3.0.0 * @static */ static buildNote(input?: number | string | Note, options?: { duration?: number; attack?: number; release?: number; rawAttack?: number; rawRelease?: number; octaveOffset?: number; }): Note; /** * Converts an input value, which can be an unsigned integer (0-127), a note identifier, a * [`Note`]{@link Note} object or an array of the previous types, to an array of * [`Note`]{@link Note} objects. * * [`Note`]{@link Note} objects are returned as is. For note numbers and identifiers, a * [`Note`]{@link Note} object is created with the options specified. An error will be thrown when * encountering invalid input. * * Note: if both the `attack` and `rawAttack` options are specified, the later has priority. The * same goes for `release` and `rawRelease`. * * @param [notes] {number|string|Note|number[]|string[]|Note[]} * * @param {object} [options={}] * * @param {number} [options.duration=Infinity] The number of milliseconds before the note should * be explicitly stopped. * * @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If * you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `attack` and `rawAttack` are specified, the latter has precedence. * * @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and * 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both * `release` and `rawRelease` are specified, the latter has precedence. * * @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only * used when the input value is a note identifier.** * * @returns {Note[]} * * @throws TypeError An element could not be parsed as a note. * * @since 3.0.0 * @static */ static buildNoteArray(notes?: number | string | Note | number[] | string[] | Note[], options?: { duration?: number; attack?: number; release?: number; rawAttack?: number; rawRelease?: number; octaveOffset?: number; }): Note[]; /** * Returns a number between 0 and 1 representing the ratio of the input value divided by 127 (7 * bit). The returned value is restricted between 0 and 1 even if the input is greater than 127 or * smaller than 0. * * Passing `Infinity` will return `1` and passing `-Infinity` will return `0`. Otherwise, when the * input value cannot be converted to an integer, the method returns 0. * * @param value {number} A positive integer between 0 and 127 (inclusive) * @returns {number} A number between 0 and 1 (inclusive) * @static */ static from7bitToFloat(value: number): number; /** * Returns an integer between 0 and 127 which is the result of multiplying the input value by * 127. The input value should be a number between 0 and 1 (inclusively). The returned value is * restricted between 0 and 127 even if the input is greater than 1 or smaller than 0. * * Passing `Infinity` will return `127` and passing `-Infinity` will return `0`. Otherwise, when * the input value cannot be converted to a number, the method returns 0. * * @param value {number} A positive float between 0 and 1 (inclusive) * @returns {number} A number between 0 and 127 (inclusive) * @static */ static fromFloatTo7Bit(value: number): number; /** * Extracts 7bit MSB and LSB values from the supplied float. * * @param value {number} A float between 0 and 1 * @returns {{lsb: number, msb: number}} */ static fromFloatToMsbLsb(value: number): { lsb: number; msb: number; }; /** * Combines and converts MSB and LSB values (0-127) to a float between 0 and 1. The returned value * is within between 0 and 1 even if the result is greater than 1 or smaller than 0. * * @param msb {number} The most significant byte as a integer between 0 and 127. * @param [lsb=0] {number} The least significant byte as a integer between 0 and 127. * @returns {number} A float between 0 and 1. */ static fromMsbLsbToFloat(msb: number, lsb?: number): number; /** * Returns the name of a control change message matching the specified number (0-127). Some valid * control change numbers do not have a specific name or purpose assigned in the MIDI * [spec](https://midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2). * In these cases, the method returns `controllerXXX` (where XXX is the number). * * @param {number} number An integer (0-127) representing the control change message * @returns {string|undefined} The matching control change name or `undefined` if no match was * found. * * @static */ static getCcNameByNumber(number: number): string | undefined; /** * Returns the number of a control change message matching the specified name. * * @param {string} name A string representing the control change message * @returns {string|undefined} The matching control change number or `undefined` if no match was * found. * * @since 3.1 * @static */ static getCcNumberByName(name: string): number | undefined; /** * Returns the channel mode name matching the specified number. If no match is found, the function * returns `false`. * * @param {number} number An integer representing the channel mode message (120-127) * @returns {string|false} The name of the matching channel mode or `false` if no match could be * found. * * @since 2.0.0 */ static getChannelModeByNumber(number: number): string | false; /** * Given a proper note identifier (`C#4`, `Gb-1`, etc.) or a valid MIDI note number (0-127), this * method returns an object containing broken down details about the specified note (uppercase * letter, accidental and octave). * * When a number is specified, the translation to note is done using a value of 60 for middle C * (C4 = middle C). * * @param value {string|number} A note identifier A atring ("C#4", "Gb-1", etc.) or a MIDI note * number (0-127). * * @returns {{accidental: string, identifier: string, name: string, octave: number }} * * @throws TypeError Invalid note identifier * * @since 3.0.0 * @static */ static getNoteDetails(value: string | number): { accidental: string; identifier: string; name: string; octave: number; }; /** * Returns the name of the first property of the supplied object whose value is equal to the one * supplied. If nothing is found, `undefined` is returned. * * @param object {object} The object to look for the property in. * @param value {*} Any value that can be expected to be found in the object's properties. * @returns {string|undefined} The name of the matching property or `undefined` if nothing is * found. * @static */ static getPropertyByValue(object: object, value: any): string | undefined; /** * Returns a valid MIDI note number (0-127) given the specified input. The input usually is a * string containing a note identifier (`"C3"`, `"F#4"`, `"D-2"`, `"G8"`, etc.). If an integer * between 0 and 127 is passed, it will simply be returned as is (for convenience). Other strings * will be parsed for integer value, if possible. * * If the input is an identifier, the resulting note number is offset by the `octaveOffset` * parameter. For example, if you pass in "C4" (note number 60) and the `octaveOffset` value is * -2, the resulting MIDI note number will be 36. * * @param input {string|number} A string or number to extract the MIDI note number from. * @param octaveOffset {number} An integer to offset the octave by * * @returns {number|false} A valid MIDI note number (0-127) or `false` if the input could not * successfully be parsed to a note number. * * @since 3.0.0 * @static */ static guessNoteNumber(input: string | number, octaveOffset: number): number | false; /** * Indicates whether the execution environment is Node.js (`true`) or not (`false`) * @type {boolean} */ static get isNode(): boolean; /** * Indicates whether the execution environment is a browser (`true`) or not (`false`) * @type {boolean} */ static get isBrowser(): boolean; /** * Returns the supplied MIDI note number offset by the requested octave and semitone values. If * the calculated value is less than 0, 0 will be returned. If the calculated value is more than * 127, 127 will be returned. If an invalid offset value is supplied, 0 will be used. * * @param number {number} The MIDI note to offset as an integer between 0 and 127. * @param octaveOffset {number} An integer to offset the note by (in octave) * @param octaveOffset {number} An integer to offset the note by (in semitones) * @returns {number} An integer between 0 and 127 * * @throws {Error} Invalid note number * @static */ static offsetNumber(number: number, octaveOffset?: number, semitoneOffset?: number): number; /** * Returns a sanitized array of valid MIDI channel numbers (1-16). The parameter should be a * single integer or an array of integers. * * For backwards-compatibility, passing `undefined` as a parameter to this method results in all * channels being returned (1-16). Otherwise, parameters that cannot successfully be parsed to * integers between 1 and 16 are silently ignored. * * @param [channel] {number|number[]} An integer or an array of integers to parse as channel * numbers. * * @returns {number[]} An array of 0 or more valid MIDI channel numbers. * * @since 3.0.0 * @static */ static sanitizeChannels(channel?: number | number[]): number[]; /** * Returns an identifier string representing a note name (with optional accidental) followed by an * octave number. The octave can be offset by using the `octaveOffset` parameter. * * @param {number} number The MIDI note number to convert to a note identifier * @param {number} octaveOffset An offset to apply to the resulting octave * * @returns {string} * * @throws RangeError Invalid note number * @throws RangeError Invalid octaveOffset value * * @since 3.0.0 * @static */ static toNoteIdentifier(number: number, octaveOffset: number): string; /** * Returns a MIDI note number matching the identifier passed in the form of a string. The * identifier must include the octave number. The identifier also optionally include a sharp (#), * a double sharp (##), a flat (b) or a double flat (bb) symbol. For example, these are all valid * identifiers: C5, G4, D#-1, F0, Gb7, Eb-1, Abb4, B##6, etc. * * When converting note identifiers to numbers, C4 is considered to be middle C (MIDI note number * 60) as per the scientific pitch notation standard. * * The resulting note number can be offset by using the `octaveOffset` parameter. * * @param identifier {string} The identifier in the form of a letter, followed by an optional "#", * "##", "b" or "bb" followed by the octave number. For exemple: C5, G4, D#-1, F0, Gb7, Eb-1, * Abb4, B##6, etc. * * @param {number} [octaveOffset=0] A integer to offset the octave by. * * @returns {number} The MIDI note number (an integer between 0 and 127). * * @throws RangeError Invalid 'octaveOffset' value * * @throws TypeError Invalid note identifier * * @license Apache-2.0 * @since 3.0.0 * @static */ static toNoteNumber(identifier: string, octaveOffset?: number): number; /** * Returns a valid timestamp, relative to the navigation start of the document, derived from the * `time` parameter. If the parameter is a string starting with the "+" sign and followed by a * number, the resulting timestamp will be the sum of the current timestamp plus that number. If * the parameter is a positive number, it will be returned as is. Otherwise, false will be * returned. * * @param [time] {number|string} The time string (e.g. `"+2000"`) or number to parse * @return {number|false} A positive number or `false` (if the time cannot be converted) * * @since 3.0.0 * @static */ static toTimestamp(time?: number | string): number | false; } /** * The `WebMidi` object makes it easier to work with the low-level Web MIDI API. Basically, it * simplifies sending outgoing MIDI messages and reacting to incoming MIDI messages. * * When using the WebMidi.js library, you should know that the `WebMidi` class has already been * instantiated. You cannot instantiate it yourself. If you use the **IIFE** version, you should * simply use the global object called `WebMidi`. If you use the **CJS** (CommonJS) or **ESM** (ES6 * module) version, you get an already-instantiated object when you import the module. * * @fires WebMidi#connected * @fires WebMidi#disabled * @fires WebMidi#disconnected * @fires WebMidi#enabled * @fires WebMidi#error * @fires WebMidi#midiaccessgranted * @fires WebMidi#portschanged * * @extends EventEmitter * @license Apache-2.0 */ declare class WebMidi extends EventEmitter { /** * The WebMidi class is a singleton and you cannot instantiate it directly. It has already been * instantiated for you. */ constructor(); private _destroyInputsAndOutputs; private _disconnectedInputs; private _disconnectedOutputs; private _inputs; private _octaveOffset; private _onInterfaceStateChange; private _outputs; private _updateInputs; private _updateInputsAndOutputs; private _updateOutputs; private _stateChangeQueue; /** * Object containing system-wide default values that can be changed to customize how the library * works. * * @type {object} * * @property {object} defaults.note - Default values relating to note * @property {number} defaults.note.attack - A number between 0 and 127 representing the * default attack velocity of notes. Initial value is 64. * @property {number} defaults.note.release - A number between 0 and 127 representing the * default release velocity of notes. Initial value is 64. * @property {number} defaults.note.duration - A number representing the default duration of * notes (in seconds). Initial value is Infinity. */ defaults: object; /** * The [`MIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) * instance used to talk to the lower-level Web MIDI API. This should not be used directly * unless you know what you are doing. * * @type {WebMidiApi.MIDIAccess} * @readonly */ interface: WebMidiApi.MIDIAccess; /** * Indicates whether argument validation and backwards-compatibility checks are performed * throughout the WebMidi.js library for object methods and property setters. * * This is an advanced setting that should be used carefully. Setting `validation` to `false` * improves performance but should only be done once the project has been thoroughly tested with * `validation` turned on. * * @type {boolean} */ validation: boolean; /** * Adds an event listener that will trigger a function callback when the specified event is * dispatched. * * Here are the events you can listen to: connected, disabled, disconnected, enabled, * midiaccessgranted, portschanged, error * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @param {number} [options.remaining=Infinity] The number of times after which the callback * should automatically be removed. * * @returns {Listener} The listener object that was created */ addListener( e: Symbol | T, listener: WebMidiEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; "remaining"?: number; } ): Listener; /** * Adds a one-time event listener that will trigger a function callback when the specified event * is dispatched. * * Here are the events you can listen to: connected, disabled, disconnected, enabled, * midiaccessgranted, portschanged, error * * @param event {string | Symbol} The type of the event. * * @param listener {EventEmitterCallback} A callback function to execute when the specified event * is detected. This function will receive an event parameter object. For details on this object's * properties, check out the documentation for the various events (links above). * * @param {object} [options={}] * * @param {array} [options.arguments] An array of arguments which will be passed separately to the * callback function. This array is stored in the [`arguments`](Listener#arguments) property of * the [`Listener`](Listener) object and can be retrieved or modified as desired. * * @param {object} [options.context=this] The value of `this` in the callback function. * * @param {number} [options.duration=Infinity] The number of milliseconds before the listener * automatically expires. * * @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning * of the listeners array and thus be triggered before others. * * @returns {Listener} The listener object that was created */ addOneTimeListener( e: Symbol | T, listener: WebMidiEventMap[T], options?: { "arguments"?: any[]; "context"?: any; "duration"?: number; "prepend"?: boolean; } ): Listener; /** * Completely disables **WebMidi.js** by unlinking the MIDI subsystem's interface and closing all * [`Input`](Input) and [`Output`](Output) objects that may have been opened. This also means that * listeners added to [`Input`](Input) objects, [`Output`](Output) objects or to `WebMidi` itself * are also destroyed. * * @async * @returns {Promise} * * @throws {Error} The Web MIDI API is not supported by your environment. * * @since 2.0.0 */ disable(): Promise; /** * Checks if the Web MIDI API is available in the current environment and then tries to connect to * the host's MIDI subsystem. This is an asynchronous operation and it causes a security prompt to * be displayed to the user. * * To enable the use of MIDI system exclusive messages, the `sysex` option should be set to * `true`. However, under some environments (e.g. Jazz-Plugin), the `sysex` option is ignored * and system exclusive messages are always enabled. You can check the * [`sysexEnabled`](#sysexEnabled) property to confirm. * * To enable access to software synthesizers available on the host, you would set the `software` * option to `true`. However, this option is only there to future-proof the library as support for * software synths has not yet been implemented in any browser (as of September 2021). * * By the way, if you call the [`enable()`](#enable) method while WebMidi.js is already enabled, * the callback function will be executed (if any), the promise will resolve but the events * ([`"midiaccessgranted"`](#event:midiaccessgranted), [`"connected"`](#event:connected) and * [`"enabled"`](#event:enabled)) will not be fired. * * There are 3 ways to execute code after `WebMidi` has been enabled: * * - Pass a callback function in the `options` * - Listen to the [`"enabled"`](#event:enabled) event * - Wait for the promise to resolve * * In order, this is what happens towards the end of the enabling process: * * 1. [`"midiaccessgranted"`](#event:midiaccessgranted) event is triggered once the user has * granted access to use MIDI. * 2. [`"connected"`](#event:connected) events are triggered (for each available input and output) * 3. [`"enabled"`](#event:enabled) event is triggered when WebMidi.js is fully ready * 4. specified callback (if any) is executed * 5. promise is resolved and fulfilled with the `WebMidi` object. * * **Important note**: starting with Chrome v77, a page using Web MIDI API must be hosted on a * secure origin (`https://`, `localhost` or `file:///`) and the user will always be prompted to * authorize the operation (no matter if the `sysex` option is `true` or not). * * ##### Example * ```js * // Enabling WebMidi and using the promise * WebMidi.enable().then(() => { * console.log("WebMidi.js has been enabled!"); * }) * ``` * * @param [options] {object} * * @param [options.callback] {function} A function to execute once the operation completes. This * function will receive an `Error` object if enabling the Web MIDI API failed. * * @param [options.sysex=false] {boolean} Whether to enable MIDI system exclusive messages or not. * * @param [options.validation=true] {boolean} Whether to enable library-wide validation of method * arguments and setter values. This is an advanced setting that should be used carefully. Setting * [`validation`](#validation) to `false` improves performance but should only be done once the * project has been thoroughly tested with [`validation`](#validation) turned on. * * @param [options.software=false] {boolean} Whether to request access to software synthesizers on * the host system. This is part of the spec but has not yet been implemented by most browsers as * of April 2020. * * @async * * @returns {Promise.} The promise is fulfilled with the `WebMidi` object for * chainability * * @throws {Error} The Web MIDI API is not supported in your environment. * @throws {Error} Jazz-Plugin must be installed to use WebMIDIAPIShim. */ enable(options?: { callback?: Function; sysex?: boolean; validation?: boolean; software?: boolean; requestMIDIAccessFunction?: Function; }): Promise; /** * Returns the [`Input`](Input) object that matches the specified ID string or `false` if no * matching input is found. As per the Web MIDI API specification, IDs are strings (not integers). * * Please note that IDs change from one host to another. For example, Chrome does not use the same * kind of IDs as Jazz-Plugin. * * @param id {string} The ID string of the input. IDs can be viewed by looking at the * [`WebMidi.inputs`](WebMidi#inputs) array. Even though they sometimes look like integers, IDs * are strings. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected input * * @returns {Input} An [`Input`](Input) object matching the specified ID string or `undefined` * if no matching input can be found. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getInputById(id: string, options?: { disconnected?: boolean; }): Input; /** * Returns the first [`Input`](Input) object whose name **contains** the specified string. Note * that the port names change from one environment to another. For example, Chrome does not report * input names in the same way as the Jazz-Plugin does. * * @param name {string} The non-empty string to look for within the name of MIDI inputs (such as * those visible in the [inputs](WebMidi#inputs) array). * * @returns {Input | undefined} The [`Input`](Input) that was found or `undefined` if no input contained the * specified name. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected input * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getInputByName(name: string, options?: { disconnected?: boolean; }): Input | undefined; /** * Returns the [`Output`](Output) object that matches the specified ID string or `false` if no * matching output is found. As per the Web MIDI API specification, IDs are strings (not * integers). * * Please note that IDs change from one host to another. For example, Chrome does not use the same * kind of IDs as Jazz-Plugin. * * @param id {string} The ID string of the port. IDs can be viewed by looking at the * [`WebMidi.outputs`](WebMidi#outputs) array. * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected output * * @returns {Output | undefined} An [`Output`](Output) object matching the specified ID string. If no * matching output can be found, the method returns `undefined`. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getOutputById(id: string, options?: { disconnected?: boolean; }): Output | undefined; /** * Returns the first [`Output`](Output) object whose name **contains** the specified string. Note * that the port names change from one environment to another. For example, Chrome does not report * input names in the same way as the Jazz-Plugin does. * * @param name {string} The non-empty string to look for within the name of MIDI inputs (such as * those visible in the [`outputs`](#outputs) array). * @param [options] {object} * @param [options.disconnected] {boolean} Whether to retrieve a disconnected output * * @returns {Output} The [`Output`](Output) that was found or `undefined` if no output matched * the specified name. * * @throws {Error} WebMidi is not enabled. * * @since 2.0.0 */ getOutputByName(name: string, options?: { disconnected?: boolean; }): Output; /** * Checks if the specified event type is already defined to trigger the specified callback * function. * * @param event {string|Symbol} The type of the event. * * @param listener {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @returns {boolean} Boolean value indicating whether or not the `Input` or `InputChannel` * already has this listener defined. */ hasListener( e: Symbol | T, listener: WebMidiEventMap[T] ): boolean; /** * Removes the specified listener for the specified event. If no listener is specified, all * listeners for the specified event will be removed. * * @param [type] {string} The type of the event. * * @param [listener] {EventEmitterCallback} The callback function to check for. * * @param {object} [options={}] * * @param {*} [options.context] Only remove the listeners that have this exact context. * * @param {number} [options.remaining] Only remove the listener if it has exactly that many * remaining times to be executed. */ removeListener( type?: Symbol | T, listener?: WebMidiEventMap[T], options?: { "context"?: any; "remaining"?: number; } ): void; /** * Indicates whether access to the host's MIDI subsystem is active or not. * * @readonly * @type {boolean} */ get enabled(): boolean; /** * An array of all currently available MIDI inputs. * * @readonly * @type {Input[]} */ get inputs(): Input[]; /** * An integer to offset the octave of notes received from external devices or sent to external * devices. * * When a MIDI message comes in on an input channel the reported note name will be offset. For * example, if the `octaveOffset` is set to `-1` and a [`"noteon"`](InputChannel#event:noteon) * message with MIDI number 60 comes in, the note will be reported as C3 (instead of C4). * * By the same token, when [`OutputChannel.playNote()`](OutputChannel#playNote) is called, the * MIDI note number being sent will be offset. If `octaveOffset` is set to `-1`, the MIDI note * number sent will be 72 (instead of 60). * * @type {number} * * @since 2.1 */ set octaveOffset(arg: number); get octaveOffset(): number; /** * An array of all currently available MIDI outputs as [`Output`](Output) objects. * * @readonly * @type {Output[]} */ get outputs(): Output[]; /** * Indicates whether the environment provides support for the Web MIDI API or not. * * **Note**: in environments that do not offer built-in MIDI support, this will report `true` if * the * [`navigator.requestMIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) * function is available. For example, if you have installed WebMIDIAPIShim.js but no plugin, this * property will be `true` even though actual support might not be there. * * @readonly * @type {boolean} */ get supported(): boolean; /** * Indicates whether MIDI system exclusive messages have been activated when WebMidi.js was * enabled via the [`enable()`](#enable) method. * * @readonly * @type boolean */ get sysexEnabled(): boolean; /** * The elapsed time, in milliseconds, since the time * [origin](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin). * Said simply, it is the number of milliseconds that passed since the page was loaded. Being a * floating-point number, it has sub-millisecond accuracy. According to the * [documentation](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp), the * time should be accurate to 5 µs (microseconds). However, due to various constraints, the * browser might only be accurate to one millisecond. * * Note: `WebMidi.time` is simply an alias to `performance.now()`. * * @type {DOMHighResTimeStamp} * @readonly */ get time(): number; /** * The version of the library as a [semver](https://semver.org/) string. * * @readonly * @type string */ get version(): string; /** * The flavour of the library. Can be one of: * * * `esm`: ECMAScript Module * * `cjs`: CommonJS Module * * `iife`: Immediately-Invoked Function Expression * * @readonly * @type string * @since 3.0.25 */ get flavour(): string; } declare const wm: WebMidi; export { wm as WebMidi }; /** * The callback function is executed when the associated event is triggered via [`emit()`](#emit). * The [`emit()`](#emit) method relays all additional arguments it received to the callback * functions. Since [`emit()`](#emit) can be passed a variable number of arguments, it is up to * the developer to make sure the arguments match those of the associated callback. In addition, * the callback also separately receives all the arguments present in the listener's * [`arguments`](Listener#arguments) property. This makes it easy to pass data from where the * listener is added to where the listener is executed. * * @callback EventEmitterCallback * @param {...*} [args] A variable number of arguments matching the ones (if any) that were passed * to the [`emit()`](#emit) method (except, the first one) followed by the arguments found in the * listener's [`arguments`](Listener#arguments) array. */ export type EventEmitterCallback = (...args: any[]) => void; /** * The `Event` object is transmitted when state change events occur. * * WebMidi * * disabled * * enabled * * midiaccessgranted * * @property {WebMidi} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * */ export interface Event { target: Input | InputChannel | Output | WebMidi; timestamp: DOMHighResTimeStamp; type: string; } /** * The `ErrorEvent` object is transmitted when an error occurs. Only the `WebMidi` object dispatches * this type of event. * * @property {Input} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * * @property {*} error The type of the event * */ export interface ErrorEvent extends Event { error: any; target: WebMidi; } /** * The `PortEvent` object is transmitted when an event occurs on an `Input` or `Output` port. * * Input * * closed * * disconnected * * opened * * Output * * closed * * disconnected * * opened * * WebMidi * * connected * * disconnected * * portschanged * * @property {Input|Output} port The `Input` or `Output` that triggered the event * @property {Input | InputChannel | Output | WebMidi} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * */ export interface PortEvent extends Event { // port: Input | Output; port: any; // temporary fix, see issue #229 } /** * The `MessageEvent` object is transmitted when a MIDI message has been received. These events * are dispatched by `Input` and `InputChannel` classes: * * `Input`: activesensing, clock, continue, midimessage, reset, songposition, songselect, start, * stop, sysex, timecode, tunerequest, unknownmessage * * `InputChannel`: allnotesoff, allsoundoff, midimessage, resetallcontrollers, channelaftertouch, * localcontrol, monomode, omnimode, pitchbend, programchange * * @property {Input} port The `Input` that triggered the event. * @property {Input | InputChannel} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * * @property {Message} message An object containing details about the actual MIDI message content. * @property {number} [rawValue] The raw value of the message (if any) between 0-127. * @property {number | boolean} [value] The value of the message (if any) */ export interface MessageEvent extends PortEvent { message: Message; port: Input; rawValue?: number; target: Input | InputChannel; value?: number | boolean; data: Uint8Array; rawData: Uint8Array; statusByte: Number; dataBytes: Uint8Array; } /** * The `ControlChangeMessageEvent` object is transmitted when a control change MIDI message has been * received. There is a general `controlchange` event and a specific `controlchange-controllerxxx` * for each controller. * * @property {Input} port The `Input` that triggered the event. * @property {Input | InputChannel} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * * @property {Message} message An object containing details about the actual MIDI message content. * @property {number} [rawValue] The raw value of the message (if any) between 0-127. * @property {number | boolean} [value] The value of the message (if any) * * @property {object} controller * @property {string} controller.name The name of the controller * @property {number} controller.number The number of the controller (between 0-127) * @property {string} controller.description Uesr-friendly representation of the controller's * default function. * @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb` * @property {string} [subtype] The actual controller event type */ export interface ControlChangeMessageEvent extends MessageEvent { controller: { name: string; number: number; description: string; position: string; }; port: Input; subtype?: string; target: Input | InputChannel; } /** * The `NoteMessageEvent` object is transmitted when a note-related MIDI message (`noteoff`, * `noteon` or `keyaftertouch`) is received on an input channel * * @property {Input} port The `Input` that triggered the event. * @property {Input | InputChannel} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * * @property {Message} message An object containing details about the actual MIDI message content. * @property {number} [rawValue] The raw value of the message (if any) between 0-127. * @property {number | boolean} [value] The value of the message (if any) * * @property {Note} note A Note object with details about the triggered note. */ export interface NoteMessageEvent extends MessageEvent { note: Note; port: Input; target: Input | InputChannel; } /** * The `ParameterNumberMessageEvent` object is transmitted when an RPN or NRPN message is received * on an input channel. * * * nrpn * * nrpn-datadecrement * * nrpn-dataincrement * * nrpn-dataentrycoarse * * nrpn-dataentryfine * * * rpn * * rpn-datadecrement * * rpn-dataincrement * * rpn-dataentrycoarse * * rpn-dataentryfine * * @property {Input} port The `Input` that triggered the event. * @property {Input | InputChannel} target The object that dispatched the event. * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in * milliseconds since the navigation start of the document). * @property {string} type The type of the event * * @property {Message} message An object containing details about the actual MIDI message content. * @property {number} [rawValue] The raw value of the message (if any) between 0-127. * @property {number | boolean} [value] The value of the message (if any) * * @property {string} parameter The parameter * @property {number} parameterMsb The parameter' MSB value (0-127) * @property {number} parameterLsb The parameter' LSB value (0-127) * */ export interface ParameterNumberMessageEvent extends MessageEvent { parameter: string; parameterMsb: number; parameterLsb: number; port: Input; target: Input | InputChannel; } /** * A map of all the events that can be passed to `InputChannel.addListener()`. */ export interface InputChannelEventMap { // Catch-ALl "midimessage": (e: MessageEvent) => void; // Channel Mode "channelaftertouch": (e: MessageEvent) => void; "keyaftertouch": (e: NoteMessageEvent) => void; "noteoff": (e: NoteMessageEvent) => void; "noteon": (e: NoteMessageEvent) => void; "pitchbend": (e: MessageEvent) => void; "programchange": (e: MessageEvent) => void; // Control Change "controlchange": (e: ControlChangeMessageEvent) => void; "controlchange-controller0": (e: ControlChangeMessageEvent) => void; "controlchange-controller1": (e: ControlChangeMessageEvent) => void; "controlchange-controller2": (e: ControlChangeMessageEvent) => void; "controlchange-controller3": (e: ControlChangeMessageEvent) => void; "controlchange-controller4": (e: ControlChangeMessageEvent) => void; "controlchange-controller5": (e: ControlChangeMessageEvent) => void; "controlchange-controller6": (e: ControlChangeMessageEvent) => void; "controlchange-controller7": (e: ControlChangeMessageEvent) => void; "controlchange-controller8": (e: ControlChangeMessageEvent) => void; "controlchange-controller9": (e: ControlChangeMessageEvent) => void; "controlchange-controller10": (e: ControlChangeMessageEvent) => void; "controlchange-controller11": (e: ControlChangeMessageEvent) => void; "controlchange-controller12": (e: ControlChangeMessageEvent) => void; "controlchange-controller13": (e: ControlChangeMessageEvent) => void; "controlchange-controller14": (e: ControlChangeMessageEvent) => void; "controlchange-controller15": (e: ControlChangeMessageEvent) => void; "controlchange-controller16": (e: ControlChangeMessageEvent) => void; "controlchange-controller17": (e: ControlChangeMessageEvent) => void; "controlchange-controller18": (e: ControlChangeMessageEvent) => void; "controlchange-controller19": (e: ControlChangeMessageEvent) => void; "controlchange-controller20": (e: ControlChangeMessageEvent) => void; "controlchange-controller21": (e: ControlChangeMessageEvent) => void; "controlchange-controller22": (e: ControlChangeMessageEvent) => void; "controlchange-controller23": (e: ControlChangeMessageEvent) => void; "controlchange-controller24": (e: ControlChangeMessageEvent) => void; "controlchange-controller25": (e: ControlChangeMessageEvent) => void; "controlchange-controller26": (e: ControlChangeMessageEvent) => void; "controlchange-controller27": (e: ControlChangeMessageEvent) => void; "controlchange-controller28": (e: ControlChangeMessageEvent) => void; "controlchange-controller29": (e: ControlChangeMessageEvent) => void; "controlchange-controller30": (e: ControlChangeMessageEvent) => void; "controlchange-controller31": (e: ControlChangeMessageEvent) => void; "controlchange-controller32": (e: ControlChangeMessageEvent) => void; "controlchange-controller33": (e: ControlChangeMessageEvent) => void; "controlchange-controller34": (e: ControlChangeMessageEvent) => void; "controlchange-controller35": (e: ControlChangeMessageEvent) => void; "controlchange-controller36": (e: ControlChangeMessageEvent) => void; "controlchange-controller37": (e: ControlChangeMessageEvent) => void; "controlchange-controller38": (e: ControlChangeMessageEvent) => void; "controlchange-controller39": (e: ControlChangeMessageEvent) => void; "controlchange-controller40": (e: ControlChangeMessageEvent) => void; "controlchange-controller41": (e: ControlChangeMessageEvent) => void; "controlchange-controller42": (e: ControlChangeMessageEvent) => void; "controlchange-controller43": (e: ControlChangeMessageEvent) => void; "controlchange-controller44": (e: ControlChangeMessageEvent) => void; "controlchange-controller45": (e: ControlChangeMessageEvent) => void; "controlchange-controller46": (e: ControlChangeMessageEvent) => void; "controlchange-controller47": (e: ControlChangeMessageEvent) => void; "controlchange-controller48": (e: ControlChangeMessageEvent) => void; "controlchange-controller49": (e: ControlChangeMessageEvent) => void; "controlchange-controller50": (e: ControlChangeMessageEvent) => void; "controlchange-controller51": (e: ControlChangeMessageEvent) => void; "controlchange-controller52": (e: ControlChangeMessageEvent) => void; "controlchange-controller53": (e: ControlChangeMessageEvent) => void; "controlchange-controller54": (e: ControlChangeMessageEvent) => void; "controlchange-controller55": (e: ControlChangeMessageEvent) => void; "controlchange-controller56": (e: ControlChangeMessageEvent) => void; "controlchange-controller57": (e: ControlChangeMessageEvent) => void; "controlchange-controller58": (e: ControlChangeMessageEvent) => void; "controlchange-controller59": (e: ControlChangeMessageEvent) => void; "controlchange-controller60": (e: ControlChangeMessageEvent) => void; "controlchange-controller61": (e: ControlChangeMessageEvent) => void; "controlchange-controller62": (e: ControlChangeMessageEvent) => void; "controlchange-controller63": (e: ControlChangeMessageEvent) => void; "controlchange-controller64": (e: ControlChangeMessageEvent) => void; "controlchange-controller65": (e: ControlChangeMessageEvent) => void; "controlchange-controller66": (e: ControlChangeMessageEvent) => void; "controlchange-controller67": (e: ControlChangeMessageEvent) => void; "controlchange-controller68": (e: ControlChangeMessageEvent) => void; "controlchange-controller69": (e: ControlChangeMessageEvent) => void; "controlchange-controller70": (e: ControlChangeMessageEvent) => void; "controlchange-controller71": (e: ControlChangeMessageEvent) => void; "controlchange-controller72": (e: ControlChangeMessageEvent) => void; "controlchange-controller73": (e: ControlChangeMessageEvent) => void; "controlchange-controller74": (e: ControlChangeMessageEvent) => void; "controlchange-controller75": (e: ControlChangeMessageEvent) => void; "controlchange-controller76": (e: ControlChangeMessageEvent) => void; "controlchange-controller77": (e: ControlChangeMessageEvent) => void; "controlchange-controller78": (e: ControlChangeMessageEvent) => void; "controlchange-controller79": (e: ControlChangeMessageEvent) => void; "controlchange-controller80": (e: ControlChangeMessageEvent) => void; "controlchange-controller81": (e: ControlChangeMessageEvent) => void; "controlchange-controller82": (e: ControlChangeMessageEvent) => void; "controlchange-controller83": (e: ControlChangeMessageEvent) => void; "controlchange-controller84": (e: ControlChangeMessageEvent) => void; "controlchange-controller85": (e: ControlChangeMessageEvent) => void; "controlchange-controller86": (e: ControlChangeMessageEvent) => void; "controlchange-controller87": (e: ControlChangeMessageEvent) => void; "controlchange-controller88": (e: ControlChangeMessageEvent) => void; "controlchange-controller89": (e: ControlChangeMessageEvent) => void; "controlchange-controller90": (e: ControlChangeMessageEvent) => void; "controlchange-controller91": (e: ControlChangeMessageEvent) => void; "controlchange-controller92": (e: ControlChangeMessageEvent) => void; "controlchange-controller93": (e: ControlChangeMessageEvent) => void; "controlchange-controller94": (e: ControlChangeMessageEvent) => void; "controlchange-controller95": (e: ControlChangeMessageEvent) => void; "controlchange-controller96": (e: ControlChangeMessageEvent) => void; "controlchange-controller97": (e: ControlChangeMessageEvent) => void; "controlchange-controller98": (e: ControlChangeMessageEvent) => void; "controlchange-controller99": (e: ControlChangeMessageEvent) => void; "controlchange-controller100": (e: ControlChangeMessageEvent) => void; "controlchange-controller101": (e: ControlChangeMessageEvent) => void; "controlchange-controller102": (e: ControlChangeMessageEvent) => void; "controlchange-controller103": (e: ControlChangeMessageEvent) => void; "controlchange-controller104": (e: ControlChangeMessageEvent) => void; "controlchange-controller105": (e: ControlChangeMessageEvent) => void; "controlchange-controller106": (e: ControlChangeMessageEvent) => void; "controlchange-controller107": (e: ControlChangeMessageEvent) => void; "controlchange-controller108": (e: ControlChangeMessageEvent) => void; "controlchange-controller109": (e: ControlChangeMessageEvent) => void; "controlchange-controller110": (e: ControlChangeMessageEvent) => void; "controlchange-controller111": (e: ControlChangeMessageEvent) => void; "controlchange-controller112": (e: ControlChangeMessageEvent) => void; "controlchange-controller113": (e: ControlChangeMessageEvent) => void; "controlchange-controller114": (e: ControlChangeMessageEvent) => void; "controlchange-controller115": (e: ControlChangeMessageEvent) => void; "controlchange-controller116": (e: ControlChangeMessageEvent) => void; "controlchange-controller117": (e: ControlChangeMessageEvent) => void; "controlchange-controller118": (e: ControlChangeMessageEvent) => void; "controlchange-controller119": (e: ControlChangeMessageEvent) => void; "controlchange-controller120": (e: ControlChangeMessageEvent) => void; "controlchange-controller121": (e: ControlChangeMessageEvent) => void; "controlchange-controller122": (e: ControlChangeMessageEvent) => void; "controlchange-controller123": (e: ControlChangeMessageEvent) => void; "controlchange-controller124": (e: ControlChangeMessageEvent) => void; "controlchange-controller125": (e: ControlChangeMessageEvent) => void; "controlchange-controller126": (e: ControlChangeMessageEvent) => void; "controlchange-controller127": (e: ControlChangeMessageEvent) => void; // Channel Voice "allnotesoff": (e: MessageEvent) => void; "allsoundoff": (e: MessageEvent) => void; "localcontrol": (e: MessageEvent) => void; "monomode": (e: MessageEvent) => void; "omnimode": (e: MessageEvent) => void; "resetallcontrollers": (e: MessageEvent) => void; // NRPN "nrpn": (e: ParameterNumberMessageEvent) => void; "nrpn-datadecrement": (e: ParameterNumberMessageEvent) => void; "nrpn-dataincrement": (e: ParameterNumberMessageEvent) => void; "nrpn-dataentrycoarse": (e: ParameterNumberMessageEvent) => void; "nrpn-dataentryfine": (e: ParameterNumberMessageEvent) => void; // RPN "rpn": (e: ParameterNumberMessageEvent) => void; "rpn-datadecrement": (e: ParameterNumberMessageEvent) => void; "rpn-dataincrement": (e: ParameterNumberMessageEvent) => void; "rpn-dataentrycoarse": (e: ParameterNumberMessageEvent) => void; "rpn-dataentryfine": (e: ParameterNumberMessageEvent) => void; } /** * A map of all the events that can be passed to `Output.addListener()`. */ export interface PortEventMap { "closed": (e: PortEvent) => void; "disconnected": (e: PortEvent) => void; "opened": (e: PortEvent) => void; } /** * A map of all the events that can be passed to `Input.addListener()`. */ export interface InputEventMap extends PortEventMap { // System Common and System Real-Time "activesensing": (e: MessageEvent) => void; "clock": (e: MessageEvent) => void; "continue": (e: MessageEvent) => void; "reset": (e: MessageEvent) => void; "songposition": (e: MessageEvent) => void; "songselect": (e: MessageEvent) => void; "start": (e: MessageEvent) => void; "stop": (e: MessageEvent) => void; "sysex": (e: MessageEvent) => void; "timecode": (e: MessageEvent) => void; "tunerequest": (e: MessageEvent) => void; // Catch-ALl "midimessage": (e: MessageEvent) => void; "unknownmessage": (e: MessageEvent) => void; // Channel Mode "channelaftertouch": (e: MessageEvent) => void; "keyaftertouch": (e: NoteMessageEvent) => void; "noteoff": (e: NoteMessageEvent) => void; "noteon": (e: NoteMessageEvent) => void; "pitchbend": (e: MessageEvent) => void; "programchange": (e: MessageEvent) => void; // Control Change "controlchange": (e: ControlChangeMessageEvent) => void; "controlchange-controller0": (e: ControlChangeMessageEvent) => void; "controlchange-controller1": (e: ControlChangeMessageEvent) => void; "controlchange-controller2": (e: ControlChangeMessageEvent) => void; "controlchange-controller3": (e: ControlChangeMessageEvent) => void; "controlchange-controller4": (e: ControlChangeMessageEvent) => void; "controlchange-controller5": (e: ControlChangeMessageEvent) => void; "controlchange-controller6": (e: ControlChangeMessageEvent) => void; "controlchange-controller7": (e: ControlChangeMessageEvent) => void; "controlchange-controller8": (e: ControlChangeMessageEvent) => void; "controlchange-controller9": (e: ControlChangeMessageEvent) => void; "controlchange-controller10": (e: ControlChangeMessageEvent) => void; "controlchange-controller11": (e: ControlChangeMessageEvent) => void; "controlchange-controller12": (e: ControlChangeMessageEvent) => void; "controlchange-controller13": (e: ControlChangeMessageEvent) => void; "controlchange-controller14": (e: ControlChangeMessageEvent) => void; "controlchange-controller15": (e: ControlChangeMessageEvent) => void; "controlchange-controller16": (e: ControlChangeMessageEvent) => void; "controlchange-controller17": (e: ControlChangeMessageEvent) => void; "controlchange-controller18": (e: ControlChangeMessageEvent) => void; "controlchange-controller19": (e: ControlChangeMessageEvent) => void; "controlchange-controller20": (e: ControlChangeMessageEvent) => void; "controlchange-controller21": (e: ControlChangeMessageEvent) => void; "controlchange-controller22": (e: ControlChangeMessageEvent) => void; "controlchange-controller23": (e: ControlChangeMessageEvent) => void; "controlchange-controller24": (e: ControlChangeMessageEvent) => void; "controlchange-controller25": (e: ControlChangeMessageEvent) => void; "controlchange-controller26": (e: ControlChangeMessageEvent) => void; "controlchange-controller27": (e: ControlChangeMessageEvent) => void; "controlchange-controller28": (e: ControlChangeMessageEvent) => void; "controlchange-controller29": (e: ControlChangeMessageEvent) => void; "controlchange-controller30": (e: ControlChangeMessageEvent) => void; "controlchange-controller31": (e: ControlChangeMessageEvent) => void; "controlchange-controller32": (e: ControlChangeMessageEvent) => void; "controlchange-controller33": (e: ControlChangeMessageEvent) => void; "controlchange-controller34": (e: ControlChangeMessageEvent) => void; "controlchange-controller35": (e: ControlChangeMessageEvent) => void; "controlchange-controller36": (e: ControlChangeMessageEvent) => void; "controlchange-controller37": (e: ControlChangeMessageEvent) => void; "controlchange-controller38": (e: ControlChangeMessageEvent) => void; "controlchange-controller39": (e: ControlChangeMessageEvent) => void; "controlchange-controller40": (e: ControlChangeMessageEvent) => void; "controlchange-controller41": (e: ControlChangeMessageEvent) => void; "controlchange-controller42": (e: ControlChangeMessageEvent) => void; "controlchange-controller43": (e: ControlChangeMessageEvent) => void; "controlchange-controller44": (e: ControlChangeMessageEvent) => void; "controlchange-controller45": (e: ControlChangeMessageEvent) => void; "controlchange-controller46": (e: ControlChangeMessageEvent) => void; "controlchange-controller47": (e: ControlChangeMessageEvent) => void; "controlchange-controller48": (e: ControlChangeMessageEvent) => void; "controlchange-controller49": (e: ControlChangeMessageEvent) => void; "controlchange-controller50": (e: ControlChangeMessageEvent) => void; "controlchange-controller51": (e: ControlChangeMessageEvent) => void; "controlchange-controller52": (e: ControlChangeMessageEvent) => void; "controlchange-controller53": (e: ControlChangeMessageEvent) => void; "controlchange-controller54": (e: ControlChangeMessageEvent) => void; "controlchange-controller55": (e: ControlChangeMessageEvent) => void; "controlchange-controller56": (e: ControlChangeMessageEvent) => void; "controlchange-controller57": (e: ControlChangeMessageEvent) => void; "controlchange-controller58": (e: ControlChangeMessageEvent) => void; "controlchange-controller59": (e: ControlChangeMessageEvent) => void; "controlchange-controller60": (e: ControlChangeMessageEvent) => void; "controlchange-controller61": (e: ControlChangeMessageEvent) => void; "controlchange-controller62": (e: ControlChangeMessageEvent) => void; "controlchange-controller63": (e: ControlChangeMessageEvent) => void; "controlchange-controller64": (e: ControlChangeMessageEvent) => void; "controlchange-controller65": (e: ControlChangeMessageEvent) => void; "controlchange-controller66": (e: ControlChangeMessageEvent) => void; "controlchange-controller67": (e: ControlChangeMessageEvent) => void; "controlchange-controller68": (e: ControlChangeMessageEvent) => void; "controlchange-controller69": (e: ControlChangeMessageEvent) => void; "controlchange-controller70": (e: ControlChangeMessageEvent) => void; "controlchange-controller71": (e: ControlChangeMessageEvent) => void; "controlchange-controller72": (e: ControlChangeMessageEvent) => void; "controlchange-controller73": (e: ControlChangeMessageEvent) => void; "controlchange-controller74": (e: ControlChangeMessageEvent) => void; "controlchange-controller75": (e: ControlChangeMessageEvent) => void; "controlchange-controller76": (e: ControlChangeMessageEvent) => void; "controlchange-controller77": (e: ControlChangeMessageEvent) => void; "controlchange-controller78": (e: ControlChangeMessageEvent) => void; "controlchange-controller79": (e: ControlChangeMessageEvent) => void; "controlchange-controller80": (e: ControlChangeMessageEvent) => void; "controlchange-controller81": (e: ControlChangeMessageEvent) => void; "controlchange-controller82": (e: ControlChangeMessageEvent) => void; "controlchange-controller83": (e: ControlChangeMessageEvent) => void; "controlchange-controller84": (e: ControlChangeMessageEvent) => void; "controlchange-controller85": (e: ControlChangeMessageEvent) => void; "controlchange-controller86": (e: ControlChangeMessageEvent) => void; "controlchange-controller87": (e: ControlChangeMessageEvent) => void; "controlchange-controller88": (e: ControlChangeMessageEvent) => void; "controlchange-controller89": (e: ControlChangeMessageEvent) => void; "controlchange-controller90": (e: ControlChangeMessageEvent) => void; "controlchange-controller91": (e: ControlChangeMessageEvent) => void; "controlchange-controller92": (e: ControlChangeMessageEvent) => void; "controlchange-controller93": (e: ControlChangeMessageEvent) => void; "controlchange-controller94": (e: ControlChangeMessageEvent) => void; "controlchange-controller95": (e: ControlChangeMessageEvent) => void; "controlchange-controller96": (e: ControlChangeMessageEvent) => void; "controlchange-controller97": (e: ControlChangeMessageEvent) => void; "controlchange-controller98": (e: ControlChangeMessageEvent) => void; "controlchange-controller99": (e: ControlChangeMessageEvent) => void; "controlchange-controller100": (e: ControlChangeMessageEvent) => void; "controlchange-controller101": (e: ControlChangeMessageEvent) => void; "controlchange-controller102": (e: ControlChangeMessageEvent) => void; "controlchange-controller103": (e: ControlChangeMessageEvent) => void; "controlchange-controller104": (e: ControlChangeMessageEvent) => void; "controlchange-controller105": (e: ControlChangeMessageEvent) => void; "controlchange-controller106": (e: ControlChangeMessageEvent) => void; "controlchange-controller107": (e: ControlChangeMessageEvent) => void; "controlchange-controller108": (e: ControlChangeMessageEvent) => void; "controlchange-controller109": (e: ControlChangeMessageEvent) => void; "controlchange-controller110": (e: ControlChangeMessageEvent) => void; "controlchange-controller111": (e: ControlChangeMessageEvent) => void; "controlchange-controller112": (e: ControlChangeMessageEvent) => void; "controlchange-controller113": (e: ControlChangeMessageEvent) => void; "controlchange-controller114": (e: ControlChangeMessageEvent) => void; "controlchange-controller115": (e: ControlChangeMessageEvent) => void; "controlchange-controller116": (e: ControlChangeMessageEvent) => void; "controlchange-controller117": (e: ControlChangeMessageEvent) => void; "controlchange-controller118": (e: ControlChangeMessageEvent) => void; "controlchange-controller119": (e: ControlChangeMessageEvent) => void; "controlchange-controller120": (e: ControlChangeMessageEvent) => void; "controlchange-controller121": (e: ControlChangeMessageEvent) => void; "controlchange-controller122": (e: ControlChangeMessageEvent) => void; "controlchange-controller123": (e: ControlChangeMessageEvent) => void; "controlchange-controller124": (e: ControlChangeMessageEvent) => void; "controlchange-controller125": (e: ControlChangeMessageEvent) => void; "controlchange-controller126": (e: ControlChangeMessageEvent) => void; "controlchange-controller127": (e: ControlChangeMessageEvent) => void; // Channel Voice "allnotesoff": (e: MessageEvent) => void; "allsoundoff": (e: MessageEvent) => void; "localcontrol": (e: MessageEvent) => void; "monomode": (e: MessageEvent) => void; "omnimode": (e: MessageEvent) => void; "resetallcontrollers": (e: MessageEvent) => void; // NRPN "nrpn": (e: ParameterNumberMessageEvent) => void; "nrpn-datadecrement": (e: ParameterNumberMessageEvent) => void; "nrpn-dataincrement": (e: ParameterNumberMessageEvent) => void; "nrpn-dataentrycoarse": (e: ParameterNumberMessageEvent) => void; "nrpn-dataentryfine": (e: ParameterNumberMessageEvent) => void; // RPN "rpn": (e: ParameterNumberMessageEvent) => void; "rpn-datadecrement": (e: ParameterNumberMessageEvent) => void; "rpn-dataincrement": (e: ParameterNumberMessageEvent) => void; "rpn-dataentrycoarse": (e: ParameterNumberMessageEvent) => void; "rpn-dataentryfine": (e: ParameterNumberMessageEvent) => void; } /** * A map of all the events that can be passed to `Output.addListener()`. */ export interface WebMidiEventMap { "connected": (e: PortEvent) => void; "disabled": (e: Event) => void; "disconnected": (e: PortEvent) => void; "enabled": (e: Event) => void; "midiaccessgranted": (e: Event) => void; "portschanged": (e: PortEvent) => void; "error": (e: ErrorEvent) => void; } ================================================ FILE: website/.gitignore ================================================ # Dependencies /node_modules # Production /build # Generated files .docusaurus .cache-loader # Misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: website/README.md ================================================ # Website This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. ## Installation ```console yarn install ``` ## Local Development ```console yarn start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. ## Build ```console yarn build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. ## Deployment ```console GIT_USER= USE_SSH=true yarn deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. ================================================ FILE: website/api/classes/Enumerations.md ================================================ # Enumerations The `Enumerations` class contains enumerations and arrays of elements used throughout the library. All its properties are static and should be referenced using the class name. For example: `Enumerations.CHANNEL_MESSAGES`. **Since**: 3.0.0 *** ## Properties ### `.CHANNEL_EVENTS` {#CHANNEL_EVENTS} **Type**: Array.<string>
**Attributes**: read-only, static
Array of channel-specific event names that can be listened for. This includes channel mode events and RPN/NRPN events. ### `.CHANNEL_NUMBERS` {#CHANNEL_NUMBERS} **Since**: 3.1
**Type**: Array.<number>
**Attributes**: read-only, static
A simple array of the 16 valid MIDI channel numbers (`1` to `16`): ### `.CONTROL_CHANGE_MESSAGES` {#CONTROL_CHANGE_MESSAGES} **Since**: 3.1
**Type**: Array.<object>
**Attributes**: read-only, static
An array of objects, ordered by control number, describing control change messages. Each object in the array has 3 properties with some objects having a fourth one (`position`) : * `number`: MIDI control number (0-127); * `name`: name of emitted event (eg: `bankselectcoarse`, `choruslevel`, etc) that can be listened to; * `description`: user-friendly description of the controller's purpose; * `position` (optional): whether this controller's value should be considered an `msb` or `lsb` Not all controllers have a predefined function. For those that don't, `name` is the word "controller" followed by the number (e.g. `controller112`). | Event name | Control Number | |--------------------------------|----------------| | `bankselectcoarse` | 0 | | `modulationwheelcoarse` | 1 | | `breathcontrollercoarse` | 2 | | `controller3` | 3 | | `footcontrollercoarse` | 4 | | `portamentotimecoarse` | 5 | | `dataentrycoarse` | 6 | | `volumecoarse` | 7 | | `balancecoarse` | 8 | | `controller9` | 9 | | `pancoarse` | 10 | | `expressioncoarse` | 11 | | `effectcontrol1coarse` | 12 | | `effectcontrol2coarse` | 13 | | `controller14` | 14 | | `controller15` | 15 | | `generalpurposecontroller1` | 16 | | `generalpurposecontroller2` | 17 | | `generalpurposecontroller3` | 18 | | `generalpurposecontroller4` | 19 | | `controller20` | 20 | | `controller21` | 21 | | `controller22` | 22 | | `controller23` | 23 | | `controller24` | 24 | | `controller25` | 25 | | `controller26` | 26 | | `controller27` | 27 | | `controller28` | 28 | | `controller29` | 29 | | `controller30` | 30 | | `controller31` | 31 | | `bankselectfine` | 32 | | `modulationwheelfine` | 33 | | `breathcontrollerfine` | 34 | | `controller35` | 35 | | `footcontrollerfine` | 36 | | `portamentotimefine` | 37 | | `dataentryfine` | 38 | | `channelvolumefine` | 39 | | `balancefine` | 40 | | `controller41` | 41 | | `panfine` | 42 | | `expressionfine` | 43 | | `effectcontrol1fine` | 44 | | `effectcontrol2fine` | 45 | | `controller46` | 46 | | `controller47` | 47 | | `controller48` | 48 | | `controller49` | 49 | | `controller50` | 50 | | `controller51` | 51 | | `controller52` | 52 | | `controller53` | 53 | | `controller54` | 54 | | `controller55` | 55 | | `controller56` | 56 | | `controller57` | 57 | | `controller58` | 58 | | `controller59` | 59 | | `controller60` | 60 | | `controller61` | 61 | | `controller62` | 62 | | `controller63` | 63 | | `damperpedal` | 64 | | `portamento` | 65 | | `sostenuto` | 66 | | `softpedal` | 67 | | `legatopedal` | 68 | | `hold2` | 69 | | `soundvariation` | 70 | | `resonance` | 71 | | `releasetime` | 72 | | `attacktime` | 73 | | `brightness` | 74 | | `decaytime` | 75 | | `vibratorate` | 76 | | `vibratodepth` | 77 | | `vibratodelay` | 78 | | `controller79` | 79 | | `generalpurposecontroller5` | 80 | | `generalpurposecontroller6` | 81 | | `generalpurposecontroller7` | 82 | | `generalpurposecontroller8` | 83 | | `portamentocontrol` | 84 | | `controller85` | 85 | | `controller86` | 86 | | `controller87` | 87 | | `highresolutionvelocityprefix` | 88 | | `controller89` | 89 | | `controller90` | 90 | | `effect1depth` | 91 | | `effect2depth` | 92 | | `effect3depth` | 93 | | `effect4depth` | 94 | | `effect5depth` | 95 | | `dataincrement` | 96 | | `datadecrement` | 97 | | `nonregisteredparameterfine` | 98 | | `nonregisteredparametercoarse` | 99 | | `nonregisteredparameterfine` | 100 | | `registeredparametercoarse` | 101 | | `controller102` | 102 | | `controller103` | 103 | | `controller104` | 104 | | `controller105` | 105 | | `controller106` | 106 | | `controller107` | 107 | | `controller108` | 108 | | `controller109` | 109 | | `controller110` | 110 | | `controller111` | 111 | | `controller112` | 112 | | `controller113` | 113 | | `controller114` | 114 | | `controller115` | 115 | | `controller116` | 116 | | `controller117` | 117 | | `controller118` | 118 | | `controller119` | 119 | | `allsoundoff` | 120 | | `resetallcontrollers` | 121 | | `localcontrol` | 122 | | `allnotesoff` | 123 | | `omnimodeoff` | 124 | | `omnimodeon` | 125 | | `monomodeon` | 126 | | `polymodeon` | 127 | *** ## Enums ### `.CHANNEL_MESSAGES` {#CHANNEL_MESSAGES} **Type**: Object.<string, number>
**Attributes**: static Enumeration of all MIDI channel message names and their associated 4-bit numerical value: | Message Name | Hexadecimal | Decimal | |---------------------|-------------|---------| | `noteoff` | 0x8 | 8 | | `noteon` | 0x9 | 9 | | `keyaftertouch` | 0xA | 10 | | `controlchange` | 0xB | 11 | | `programchange` | 0xC | 12 | | `channelaftertouch` | 0xD | 13 | | `pitchbend` | 0xE | 14 | ### `.CHANNEL_MODE_MESSAGES` {#CHANNEL_MODE_MESSAGES} **Type**: Object.<string, number>
**Attributes**: static Enumeration of all MIDI channel mode message names and their associated numerical value: | Message Name | Hexadecimal | Decimal | |-----------------------|-------------|---------| | `allsoundoff` | 0x78 | 120 | | `resetallcontrollers` | 0x79 | 121 | | `localcontrol` | 0x7A | 122 | | `allnotesoff` | 0x7B | 123 | | `omnimodeoff` | 0x7C | 124 | | `omnimodeon` | 0x7D | 125 | | `monomodeon` | 0x7E | 126 | | `polymodeon` | 0x7F | 127 | ### `.REGISTERED_PARAMETERS` {#REGISTERED_PARAMETERS} **Type**: Object.<string, Array.<number>>
**Attributes**: static Enumeration of all MIDI registered parameters and their associated pair of numerical values. MIDI registered parameters extend the original list of control change messages. Currently, there are only a limited number of them: | Control Function | [LSB, MSB] | |------------------------------|--------------| | `pitchbendrange` | [0x00, 0x00] | | `channelfinetuning` | [0x00, 0x01] | | `channelcoarsetuning` | [0x00, 0x02] | | `tuningprogram` | [0x00, 0x03] | | `tuningbank` | [0x00, 0x04] | | `modulationrange` | [0x00, 0x05] | | `azimuthangle` | [0x3D, 0x00] | | `elevationangle` | [0x3D, 0x01] | | `gain` | [0x3D, 0x02] | | `distanceratio` | [0x3D, 0x03] | | `maximumdistance` | [0x3D, 0x04] | | `maximumdistancegain` | [0x3D, 0x05] | | `referencedistanceratio` | [0x3D, 0x06] | | `panspreadangle` | [0x3D, 0x07] | | `rollangle` | [0x3D, 0x08] | ### `.SYSTEM_MESSAGES` {#SYSTEM_MESSAGES} **Type**: Object.<string, number>
**Attributes**: static Enumeration of all valid MIDI system messages and matching numerical values. This library also uses two additional custom messages. **System Common Messages** | Function | Hexadecimal | Decimal | |------------------------|-------------|---------| | `sysex` | 0xF0 | 240 | | `timecode` | 0xF1 | 241 | | `songposition` | 0xF2 | 242 | | `songselect` | 0xF3 | 243 | | `tunerequest` | 0xF6 | 246 | | `sysexend` | 0xF7 | 247 | The `sysexend` message is never actually received. It simply ends a sysex stream. **System Real-Time Messages** | Function | Hexadecimal | Decimal | |------------------------|-------------|---------| | `clock` | 0xF8 | 248 | | `start` | 0xFA | 250 | | `continue` | 0xFB | 251 | | `stop` | 0xFC | 252 | | `activesensing` | 0xFE | 254 | | `reset` | 0xFF | 255 | Values 249 and 253 are relayed by the [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) but they do not serve any specific purpose. The [MIDI 1.0 spec](https://www.midi.org/specifications/item/table-1-summary-of-midi-message) simply states that they are undefined/reserved. **Custom Messages** These two messages are mostly for internal use. They are not MIDI messages and cannot be sent or forwarded. | Function | Hexadecimal | Decimal | |------------------------|-------------|---------| | `midimessage` | | 0 | | `unknownsystemmessage` | | -1 | ================================================ FILE: website/api/classes/EventEmitter.md ================================================ # EventEmitter The `EventEmitter` class provides methods to implement the _observable_ design pattern. This pattern allows one to _register_ a function to execute when a specific event is _emitted_ by the emitter. It is intended to be an abstract class meant to be extended by (or mixed into) other objects. ### `Constructor` Creates a new `EventEmitter`object. **Parameters** > `new EventEmitter([eventsSuspended])`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`eventsSuspended`**] | boolean
|false|Whether the `EventEmitter` is initially in a suspended state (i.e. not executing callbacks).|
*** ## Properties ### `.ANY_EVENT` {#ANY_EVENT} **Type**: Symbol
Identifier to use when adding or removing a listener that should be triggered when any events occur. ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. *** ## Methods ### `.addListener(...)` {#addListener} Adds a listener for the specified event. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to.| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs.| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The value of `this` in the callback function.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time listener for the specified event. The listener will be executed once and then destroyed. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addOneTimeListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The context to invoke the callback function in.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.hasListener(...)` {#hasListener} Returns `true` if the specified event has at least one registered listener. If no event is specified, the method returns `true` if any event has at least one listener registered (this includes global listeners registered to [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). Note: to specifically check for global listeners added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT), use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `hasListener([event], [callback])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
Symbol
|(any event)|The event to check| |[**`callback`**] | function
Listener
|(any callback)|The actual function that was added to the event or the [Listener](Listener) object returned by `addListener()`.|
**Return Value** > Returns: `boolean`
### `.removeListener(...)` {#removeListener} Removes all the listeners that were added to the object upon which the method is called and that match the specified criterias. If no parameters are passed, all listeners added to this object will be removed. If only the `event` parameter is passed, all listeners for that event will be removed from that object. You can remove global listeners by using [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. To use more granular options, you must at least define the `event`. Then, you can specify the callback to match or one or more of the additional options. **Parameters** > Signature: `removeListener([event], [callback], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
||The event name.| |[**`callback`**] | EventEmitter~callback
||Only remove the listeners that match this exact callback function.| |[**`options`**] | Object
||| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
================================================ FILE: website/api/classes/Forwarder.md ================================================ # Forwarder The `Forwarder` class allows the forwarding of MIDI messages to predetermined outputs. When you call its [`forward()`](#forward) method, it will send the specified [`Message`](Message) object to all the outputs listed in its [`destinations`](#destinations) property. If specific channels or message types have been defined in the [`channels`](#channels) or [`types`](#types) properties, only messages matching the channels/types will be forwarded. While it can be manually instantiated, you are more likely to come across a `Forwarder` object as the return value of the [`Input.addForwarder()`](Input#addForwarder) method. **Since**: 3.0.0 ### `Constructor` Creates a `Forwarder` object. **Parameters** > `new Forwarder([destinations], [options])`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`destinations`**] | Output
Array.<Output>
|\[\]|An [`Output`](Output) object, or an array of such objects, to forward the message to.| |[**`options`**] | object
|{}|| |[**`options.types`**] | string
Array.<string>
|(all messages)|A MIDI message type or an array of such types (`"noteon"`, `"controlchange"`, etc.), that the specified message must match in order to be forwarded. If this option is not specified, all types of messages will be forwarded. Valid messages are the ones found in either [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) or [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES).| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|A MIDI channel number or an array of channel numbers that the message must match in order to be forwarded. By default all MIDI channels are included (`1` to `16`).|
*** ## Properties ### `.channels` {#channels} **Type**: Array.<number>
An array of MIDI channel numbers that the message must match in order to be forwarded. By default, this array includes all MIDI channels (`1` to `16`). ### `.destinations` {#destinations} **Type**: Array.<Output>
An array of [`Output`](Output) objects to forward the message to. ### `.suspended` {#suspended} **Type**: boolean
Indicates whether message forwarding is currently suspended or not in this forwarder. ### `.types` {#types} **Type**: Array.<string>
An array of message types (`"noteon"`, `"controlchange"`, etc.) that must be matched in order for messages to be forwarded. By default, this array includes all [`Enumerations.SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) and [`Enumerations.CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES). *** ## Methods ### `.forward(...)` {#forward} Sends the specified message to the forwarder's destination(s) if it matches the specified type(s) and channel(s). **Parameters** > Signature: `forward(message)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`message`** | Message
||The [`Message`](Message) object to forward.|
================================================ FILE: website/api/classes/Input.md ================================================ # Input The `Input` class represents a single MIDI input port. This object is automatically instantiated by the library according to the host's MIDI subsystem and does not need to be directly instantiated. Instead, you can access all `Input` objects by referring to the [`WebMidi.inputs`](WebMidi#inputs) array. You can also retrieve inputs by using methods such as [`WebMidi.getInputByName()`](WebMidi#getInputByName) and [`WebMidi.getInputById()`](WebMidi#getInputById). Note that a single MIDI device may expose several inputs and/or outputs. **Important**: the `Input` class does not directly fire channel-specific MIDI messages (such as [`noteon`](InputChannel#event:noteon) or [`controlchange`](InputChannel#event:controlchange), etc.). The [`InputChannel`](InputChannel) object does that. However, you can still use the [`Input.addListener()`](#addListener) method to listen to channel-specific events on multiple [`InputChannel`](InputChannel) objects at once. **Extends**: [`EventEmitter`](EventEmitter) **Fires**: [`activesensing`](#event:activesensing), [`clock`](#event:clock), [`closed`](#event:closed), [`continue`](#event:continue), [`disconnected`](#event:disconnected), [`midimessage`](#event:midimessage), [`opened`](#event:opened), [`reset`](#event:reset), [`songposition`](#event:songposition), [`songselect`](#event:songselect), [`start`](#event:start), [`stop`](#event:stop), [`sysex`](#event:sysex), [`timecode`](#event:timecode), [`tunerequest`](#event:tunerequest), [`unknownmidimessage`](#event:unknownmidimessage) ### `Constructor` Creates an `Input` object. **Parameters** > `new Input(midiInput)`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`midiInput`** | MIDIInput
||[`MIDIInput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput) object as provided by the MIDI subsystem (Web MIDI API).|
*** ## Properties ### `.channels` {#channels} **Type**: Array.<InputChannel>
Array containing the 16 [`InputChannel`](InputChannel) objects available for this `Input`. The channels are numbered 1 through 16. ### `.connection` {#connection} **Type**: string
**Attributes**: read-only
Input port's connection state: `pending`, `open` or `closed`. ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. ### `.id` {#id} **Type**: string
**Attributes**: read-only
ID string of the MIDI port. The ID is host-specific. Do not expect the same ID on different platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for the same port. ### `.manufacturer` {#manufacturer} **Type**: string
**Attributes**: read-only
Name of the manufacturer of the device that makes this input port available. ### `.name` {#name} **Type**: string
**Attributes**: read-only
Name of the MIDI input. ### `.octaveOffset` {#octaveOffset} **Since**: 3.0
**Type**: number
An integer to offset the reported octave of incoming notes. By default, middle C (MIDI note number 60) is placed on the 4th octave (C4). If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. Note that this value is combined with the global offset value defined in the [`WebMidi.octaveOffset`](WebMidi#octaveOffset) property (if any). ### `.state` {#state} **Type**: string
**Attributes**: read-only
State of the input port: `connected` or `disconnected`. ### `.type` {#type} **Type**: string
**Attributes**: read-only
The port type. In the case of the `Input` object, this is always: `input`. *** ## Methods ### `.addForwarder(...)` {#addForwarder} Adds a forwarder that will forward all incoming MIDI messages matching the criteria to the specified [`Output`](Output) destination(s). This is akin to the hardware MIDI THRU port, with the added benefit of being able to filter which data is forwarded. **Parameters** > Signature: `addForwarder(output, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`output`** | Output
Array.<Output>
Forwarder
||An [`Output`](Output) object, a [`Forwarder`](Forwarder) object or an array of such objects, to forward messages to.| |[**`options`**] | object
|{}|| |[**`options.types`**] | string
Array.<string>
|(all messages)|A message type, or an array of such types (`noteon`, `controlchange`, etc.), that the message type must match in order to be forwarded. If this option is not specified, all types of messages will be forwarded. Valid messages are the ones found in either [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) or [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES).| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|A MIDI channel number or an array of channel numbers that the message must match in order to be forwarded. By default all MIDI channels are included (`1` to `16`).|
**Return Value** > Returns: `Forwarder`
The [`Forwarder`](Forwarder) object created to handle the forwarding. This is useful if you wish to manipulate or remove the [`Forwarder`](Forwarder) later on. ### `.addListener(...)` {#addListener} Adds an event listener that will trigger a function callback when the specified event is dispatched. The event usually is **input-wide** but can also be **channel-specific**. Input-wide events do not target a specific MIDI channel so it makes sense to listen for them at the `Input` level and not at the [`InputChannel`](InputChannel) level. Channel-specific events target a specific channel. Usually, in this case, you would add the listener to the [`InputChannel`](InputChannel) object. However, as a convenience, you can also listen to channel-specific events directly on an `Input`. This allows you to react to a channel-specific event no matter which channel it actually came through. When listening for an event, you simply need to specify the event name and the function to execute: ```javascript const listener = WebMidi.inputs[0].addListener("midimessage", e => { console.log(e); }); ``` Calling the function with an input-wide event (such as [`"midimessage"`](#event:midimessage)), will return the [`Listener`](Listener) object that was created. If you call the function with a channel-specific event (such as [`"noteon"`](InputChannel#event:noteon)), it will return an array of all [`Listener`](Listener) objects that were created (one for each channel): ```javascript const listeners = WebMidi.inputs[0].addListener("noteon", someFunction); ``` You can also specify which channels you want to add the listener to: ```javascript const listeners = WebMidi.inputs[0].addListener("noteon", someFunction, {channels: [1, 2, 3]}); ``` In this case, `listeners` is an array containing 3 [`Listener`](Listener) objects. The order of the listeners in the array follows the order the channels were specified in. Note that, when adding channel-specific listeners, it is the [`InputChannel`](InputChannel) instance that actually gets a listener added and not the `Input` instance. You can check that by calling [`InputChannel.hasListener()`](InputChannel#hasListener()). There are 8 families of events you can listen to: 1. **MIDI System Common** Events (input-wide) * [`songposition`](Input#event:songposition) * [`songselect`](Input#event:songselect) * [`sysex`](Input#event:sysex) * [`timecode`](Input#event:timecode) * [`tunerequest`](Input#event:tunerequest) 2. **MIDI System Real-Time** Events (input-wide) * [`clock`](Input#event:clock) * [`start`](Input#event:start) * [`continue`](Input#event:continue) * [`stop`](Input#event:stop) * [`activesensing`](Input#event:activesensing) * [`reset`](Input#event:reset) 3. **State Change** Events (input-wide) * [`opened`](Input#event:opened) * [`closed`](Input#event:closed) * [`disconnected`](Input#event:disconnected) 4. **Catch-All** Events (input-wide) * [`midimessage`](Input#event:midimessage) * [`unknownmidimessage`](Input#event:unknownmidimessage) 5. **Channel Voice** Events (channel-specific) * [`channelaftertouch`](InputChannel#event:channelaftertouch) * [`controlchange`](InputChannel#event:controlchange) * [`controlchange-controller0`](InputChannel#event:controlchange-controller0) * [`controlchange-controller1`](InputChannel#event:controlchange-controller1) * [`controlchange-controller2`](InputChannel#event:controlchange-controller2) * (...) * [`controlchange-controller127`](InputChannel#event:controlchange-controller127) * [`keyaftertouch`](InputChannel#event:keyaftertouch) * [`noteoff`](InputChannel#event:noteoff) * [`noteon`](InputChannel#event:noteon) * [`pitchbend`](InputChannel#event:pitchbend) * [`programchange`](InputChannel#event:programchange) Note: you can listen for a specific control change message by using an event name like this: `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, etc. 6. **Channel Mode** Events (channel-specific) * [`allnotesoff`](InputChannel#event:allnotesoff) * [`allsoundoff`](InputChannel#event:allsoundoff) * [`localcontrol`](InputChannel#event:localcontrol) * [`monomode`](InputChannel#event:monomode) * [`omnimode`](InputChannel#event:omnimode) * [`resetallcontrollers`](InputChannel#event:resetallcontrollers) 7. **NRPN** Events (channel-specific) * [`nrpn`](InputChannel#event:nrpn) * [`nrpn-dataentrycoarse`](InputChannel#event:nrpn-dataentrycoarse) * [`nrpn-dataentryfine`](InputChannel#event:nrpn-dataentryfine) * [`nrpn-dataincrement`](InputChannel#event:nrpn-dataincrement) * [`nrpn-datadecrement`](InputChannel#event:nrpn-datadecrement) 8. **RPN** Events (channel-specific) * [`rpn`](InputChannel#event:rpn) * [`rpn-dataentrycoarse`](InputChannel#event:rpn-dataentrycoarse) * [`rpn-dataentryfine`](InputChannel#event:rpn-dataentryfine) * [`rpn-dataincrement`](InputChannel#event:rpn-dataincrement) * [`rpn-datadecrement`](InputChannel#event:rpn-datadecrement) **Parameters** > Signature: `addListener(event, listener, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
EventEmitter.ANY_EVENT
||The type of the event.| |**`listener`** | function
||A callback function to execute when the specified event is detected. This function will receive an event parameter object. For details on this object's properties, check out the documentation for the various events (links above).| |[**`options`**] | object
|{}|| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to listen on. If no channel is specified, all channels will be used. This parameter is ignored for input-wide events.| |[**`options.context`**] | object
|this|The value of `this` in the callback function.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus be triggered before others.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.|
**Return Value** > Returns: `Listener` or `Array.`
If the event is input-wide, a single [`Listener`](Listener) object is returned. If the event is channel-specific, an array of all the [`Listener`](Listener) objects is returned (one for each channel). ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time event listener that will trigger a function callback when the specified event happens. The event can be **channel-bound** or **input-wide**. Channel-bound events are dispatched by [`InputChannel`](InputChannel) objects and are tied to a specific MIDI channel while input-wide events are dispatched by the `Input` object itself and are not tied to a specific channel. Calling the function with an input-wide event (such as [`"midimessage"`](#event:midimessage)), will return the [`Listener`](Listener) object that was created. If you call the function with a channel-specific event (such as [`"noteon"`](InputChannel#event:noteon)), it will return an array of all [`Listener`](Listener) objects that were created (one for each channel): ```javascript const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction); ``` You can also specify which channels you want to add the listener to: ```javascript const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction, {channels: [1, 2, 3]}); ``` In this case, the `listeners` variable contains an array of 3 [`Listener`](Listener) objects. The code above will add a listener for the `"noteon"` event and call `someFunction` when the event is triggered on MIDI channels `1`, `2` or `3`. Note that, when adding events to channels, it is the [`InputChannel`](InputChannel) instance that actually gets a listener added and not the `Input` instance. Note: if you want to add a listener to a single MIDI channel you should probably do so directly on the [`InputChannel`](InputChannel) object itself. There are 8 families of events you can listen to: 1. **MIDI System Common** Events (input-wide) * [`songposition`](Input#event:songposition) * [`songselect`](Input#event:songselect) * [`sysex`](Input#event:sysex) * [`timecode`](Input#event:timecode) * [`tunerequest`](Input#event:tunerequest) 2. **MIDI System Real-Time** Events (input-wide) * [`clock`](Input#event:clock) * [`start`](Input#event:start) * [`continue`](Input#event:continue) * [`stop`](Input#event:stop) * [`activesensing`](Input#event:activesensing) * [`reset`](Input#event:reset) 3. **State Change** Events (input-wide) * [`opened`](Input#event:opened) * [`closed`](Input#event:closed) * [`disconnected`](Input#event:disconnected) 4. **Catch-All** Events (input-wide) * [`midimessage`](Input#event:midimessage) * [`unknownmidimessage`](Input#event:unknownmidimessage) 5. **Channel Voice** Events (channel-specific) * [`channelaftertouch`](InputChannel#event:channelaftertouch) * [`controlchange`](InputChannel#event:controlchange) * [`controlchange-controller0`](InputChannel#event:controlchange-controller0) * [`controlchange-controller1`](InputChannel#event:controlchange-controller1) * [`controlchange-controller2`](InputChannel#event:controlchange-controller2) * (...) * [`controlchange-controller127`](InputChannel#event:controlchange-controller127) * [`keyaftertouch`](InputChannel#event:keyaftertouch) * [`noteoff`](InputChannel#event:noteoff) * [`noteon`](InputChannel#event:noteon) * [`pitchbend`](InputChannel#event:pitchbend) * [`programchange`](InputChannel#event:programchange) Note: you can listen for a specific control change message by using an event name like this: `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`, etc. 6. **Channel Mode** Events (channel-specific) * [`allnotesoff`](InputChannel#event:allnotesoff) * [`allsoundoff`](InputChannel#event:allsoundoff) * [`localcontrol`](InputChannel#event:localcontrol) * [`monomode`](InputChannel#event:monomode) * [`omnimode`](InputChannel#event:omnimode) * [`resetallcontrollers`](InputChannel#event:resetallcontrollers) 7. **NRPN** Events (channel-specific) * [`nrpn`](InputChannel#event:nrpn) * [`nrpn-dataentrycoarse`](InputChannel#event:nrpn-dataentrycoarse) * [`nrpn-dataentryfine`](InputChannel#event:nrpn-dataentryfine) * [`nrpn-dataincrement`](InputChannel#event:nrpn-dataincrement) * [`nrpn-datadecrement`](InputChannel#event:nrpn-datadecrement) 8. **RPN** Events (channel-specific) * [`rpn`](InputChannel#event:rpn) * [`rpn-dataentrycoarse`](InputChannel#event:rpn-dataentrycoarse) * [`rpn-dataentryfine`](InputChannel#event:rpn-dataentryfine) * [`rpn-dataincrement`](InputChannel#event:rpn-dataincrement) * [`rpn-datadecrement`](InputChannel#event:rpn-datadecrement) **Parameters** > Signature: `addOneTimeListener(event, listener, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The type of the event.| |**`listener`** | function
||A callback function to execute when the specified event is detected. This function will receive an event parameter object. For details on this object's properties, check out the documentation for the various events (links above).| |[**`options`**] | object
|{}|| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.| |[**`options.channels`**] | number
Array.<number>
||An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to listen on. This parameter is ignored for input-wide events.| |[**`options.context`**] | object
|this|The value of `this` in the callback function.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus be triggered before others.|
**Return Value** > Returns: `Array.`
An array of all [`Listener`](Listener) objects that were created. ### `.close()` {#close} **Attributes**: async Closes the input. When an input is closed, it cannot be used to listen to MIDI messages until the input is opened again by calling [`Input.open()`](Input#open). **Note**: if what you want to do is stop events from being dispatched, you should use [`eventsSuspended`](#eventsSuspended) instead. **Return Value** > Returns: `Promise.`
The promise is fulfilled with the `Input` object ### `.destroy()` {#destroy} **Attributes**: async Destroys the `Input` by removing all listeners, emptying the [`channels`](#channels) array and unlinking the MIDI subsystem. This is mostly for internal use. **Return Value** > Returns: `Promise.`
### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.hasForwarder(...)` {#hasForwarder} Checks whether the specified [`Forwarder`](Forwarder) object has already been attached to this input. **Parameters** > Signature: `hasForwarder(forwarder)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`forwarder`** | Forwarder
||The [`Forwarder`](Forwarder) to check for (the [`Forwarder`](Forwarder) object is returned when calling [`addForwarder()`](#addForwarder).|
**Return Value** > Returns: `boolean`
### `.hasListener(...)` {#hasListener} Checks if the specified event type is already defined to trigger the specified callback function. For channel-specific events, the function will return `true` only if all channels have the listener defined. **Parameters** > Signature: `hasListener(event, listener, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The type of the event.| |**`listener`** | function
||The callback function to check for.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
||An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to check. This parameter is ignored for input-wide events.|
**Return Value** > Returns: `boolean`
Boolean value indicating whether or not the `Input` or [`InputChannel`](InputChannel) already has this listener defined. ### `.open()` {#open} **Attributes**: async Opens the input for usage. This is usually unnecessary as the port is opened automatically when WebMidi is enabled. **Return Value** > Returns: `Promise.`
The promise is fulfilled with the `Input` object. ### `.removeForwarder(...)` {#removeForwarder} Removes the specified [`Forwarder`](Forwarder) object from the input. **Parameters** > Signature: `removeForwarder(forwarder)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`forwarder`** | Forwarder
||The [`Forwarder`](Forwarder) to remove (the [`Forwarder`](Forwarder) object is returned when calling `addForwarder()`.|
### `.removeListener(...)` {#removeListener} Removes the specified event listener. If no listener is specified, all listeners matching the specified event will be removed. If the event is channel-specific, the listener will be removed from all [`InputChannel`](InputChannel) objects belonging to that channel. If no event is specified, all listeners for the `Input` as well as all listeners for all [`InputChannel`](InputChannel) objects belonging to the `Input` will be removed. By default, channel-specific listeners will be removed from all [`InputChannel`](InputChannel) objects unless the `options.channel` narrows it down. **Parameters** > Signature: `removeListener([type], [listener], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`type`**] | string
||The type of the event.| |[**`listener`**] | function
||The callback function to check for.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
||An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to match. This parameter is ignored for input-wide events.| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
*** ## Events ### `activesensing` {#event-activesensing}
Input-wide (system) event emitted when an **active sensing** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`activesensing`| ### `clock` {#event-clock} Input-wide (system) event emitted when a **timing clock** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`clock`| ### `closed` {#event-closed} Event emitted when the `Input` has been closed by calling the [`close()`](#close) method. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`closed`| |**`target`** |Input|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| ### `continue` {#event-continue} Input-wide (system) event emitted when a **continue** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`continue`| ### `disconnected` {#event-disconnected} Event emitted when the `Input` becomes unavailable. This event is typically fired when the MIDI device is unplugged. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`disconnected`| |**`port`** |Input|Object with properties describing the [Input](Input) that was disconnected. This is not the actual `Input` as it is no longer available.| |**`target`** |Input|The object that dispatched the event.| ### `midimessage` {#event-midimessage} Event emitted when any MIDI message is received on an `Input`. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`midimessage`| ### `opened` {#event-opened} Event emitted when the `Input` has been opened by calling the [`open()`](#open) method. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`opened`| |**`target`** |Input|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| ### `reset` {#event-reset} Input-wide (system) event emitted when a **reset** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`reset`| ### `songposition` {#event-songposition} Input-wide (system) event emitted when a **song position** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`songposition`| ### `songselect` {#event-songselect} Input-wide (system) event emitted when a **song select** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |string|Song (or sequence) number to select (0-127)| |**`rawValue`** |string|Song (or sequence) number to select (0-127)| ### `start` {#event-start} Input-wide (system) event emitted when a **start** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`start`| ### `stop` {#event-stop} Input-wide (system) event emitted when a **stop** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`stop`| ### `sysex` {#event-sysex} Input-wide (system) event emitted when a **system exclusive** message has been received. You should note that, to receive `sysex` events, you must call the [`WebMidi.enable()`](WebMidi#enable()) method with the `sysex` option set to `true`: ```js WebMidi.enable({sysex: true}) .then(() => console.log("WebMidi has been enabled with sysex support.")) ``` **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`sysex`| ### `timecode` {#event-timecode} Input-wide (system) event emitted when a **time code quarter frame** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`timecode`| ### `tunerequest` {#event-tunerequest} Input-wide (system) event emitted when a **tune request** message has been received. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`tunerequest`| ### `unknownmessage` {#event-unknownmessage} Input-wide (system) event emitted when an unknown MIDI message has been received. It could be, for example, one of the undefined/reserved messages. **Since**: 2.1 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`port`** |Input|The `Input` that triggered the event.| |**`target`** |Input|The object that dispatched the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`unknownmessage`| ================================================ FILE: website/api/classes/InputChannel.md ================================================ # InputChannel The `InputChannel` class represents a single MIDI input channel (1-16) from a single input device. This object is derived from the host's MIDI subsystem and should not be instantiated directly. All 16 `InputChannel` objects can be found inside the input's [`channels`](Input#channels) property. **Since**: 3.0.0 **Extends**: [`EventEmitter`](EventEmitter) **Fires**: [`allnotesoff`](#event:allnotesoff), [`allsoundoff`](#event:allsoundoff), [`channelaftertouch`](#event:channelaftertouch), [`controlchange`](#event:controlchange), [`controlchange-allnotesoff`](#event:controlchange-allnotesoff), [`controlchange-allsoundoff`](#event:controlchange-allsoundoff), [`controlchange-attacktime`](#event:controlchange-attacktime), [`controlchange-balancecoarse`](#event:controlchange-balancecoarse), [`controlchange-balancefine`](#event:controlchange-balancefine), [`controlchange-bankselectcoarse`](#event:controlchange-bankselectcoarse), [`controlchange-bankselectfine`](#event:controlchange-bankselectfine), [`controlchange-breathcontrollercoarse`](#event:controlchange-breathcontrollercoarse), [`controlchange-breathcontrollerfine`](#event:controlchange-breathcontrollerfine), [`controlchange-brightness`](#event:controlchange-brightness), [`controlchange-channelvolumefine`](#event:controlchange-channelvolumefine), [`controlchange-controllerxxx`](#event:controlchange-controllerxxx), [`controlchange-damperpedal`](#event:controlchange-damperpedal), [`controlchange-datadecrement`](#event:controlchange-datadecrement), [`controlchange-dataentrycoarse`](#event:controlchange-dataentrycoarse), [`controlchange-dataentryfine`](#event:controlchange-dataentryfine), [`controlchange-dataincrement`](#event:controlchange-dataincrement), [`controlchange-decaytime`](#event:controlchange-decaytime), [`controlchange-effect1depth`](#event:controlchange-effect1depth), [`controlchange-effect2depth`](#event:controlchange-effect2depth), [`controlchange-effect3depth`](#event:controlchange-effect3depth), [`controlchange-effect4depth`](#event:controlchange-effect4depth), [`controlchange-effect5depth`](#event:controlchange-effect5depth), [`controlchange-effectcontrol1coarse`](#event:controlchange-effectcontrol1coarse), [`controlchange-effectcontrol1fine`](#event:controlchange-effectcontrol1fine), [`controlchange-effectcontrol2coarse`](#event:controlchange-effectcontrol2coarse), [`controlchange-effectcontrol2fine`](#event:controlchange-effectcontrol2fine), [`controlchange-expressioncoarse`](#event:controlchange-expressioncoarse), [`controlchange-expressionfine`](#event:controlchange-expressionfine), [`controlchange-footcontrollercoarse`](#event:controlchange-footcontrollercoarse), [`controlchange-footcontrollerfine`](#event:controlchange-footcontrollerfine), [`controlchange-generalpurposecontroller1`](#event:controlchange-generalpurposecontroller1), [`controlchange-generalpurposecontroller2`](#event:controlchange-generalpurposecontroller2), [`controlchange-generalpurposecontroller3`](#event:controlchange-generalpurposecontroller3), [`controlchange-generalpurposecontroller4`](#event:controlchange-generalpurposecontroller4), [`controlchange-generalpurposecontroller5`](#event:controlchange-generalpurposecontroller5), [`controlchange-generalpurposecontroller6`](#event:controlchange-generalpurposecontroller6), [`controlchange-generalpurposecontroller7`](#event:controlchange-generalpurposecontroller7), [`controlchange-generalpurposecontroller8`](#event:controlchange-generalpurposecontroller8), [`controlchange-highresolutionvelocityprefix`](#event:controlchange-highresolutionvelocityprefix), [`controlchange-hold2`](#event:controlchange-hold2), [`controlchange-legatopedal`](#event:controlchange-legatopedal), [`controlchange-localcontrol`](#event:controlchange-localcontrol), [`controlchange-modulationwheelcoarse`](#event:controlchange-modulationwheelcoarse), [`controlchange-modulationwheelfine`](#event:controlchange-modulationwheelfine), [`controlchange-monomodeon`](#event:controlchange-monomodeon), [`controlchange-nonregisteredparametercoarse`](#event:controlchange-nonregisteredparametercoarse), [`controlchange-nonregisteredparameterfine`](#event:controlchange-nonregisteredparameterfine), [`controlchange-omnimodeoff`](#event:controlchange-omnimodeoff), [`controlchange-omnimodeon`](#event:controlchange-omnimodeon), [`controlchange-pancoarse`](#event:controlchange-pancoarse), [`controlchange-panfine`](#event:controlchange-panfine), [`controlchange-polymodeon`](#event:controlchange-polymodeon), [`controlchange-portamento`](#event:controlchange-portamento), [`controlchange-portamentocontrol`](#event:controlchange-portamentocontrol), [`controlchange-portamentotimecoarse`](#event:controlchange-portamentotimecoarse), [`controlchange-portamentotimefine`](#event:controlchange-portamentotimefine), [`controlchange-registeredparametercoarse`](#event:controlchange-registeredparametercoarse), [`controlchange-registeredparameterfine`](#event:controlchange-registeredparameterfine), [`controlchange-releasetime`](#event:controlchange-releasetime), [`controlchange-resetallcontrollers`](#event:controlchange-resetallcontrollers), [`controlchange-resonance`](#event:controlchange-resonance), [`controlchange-softpedal`](#event:controlchange-softpedal), [`controlchange-sostenuto`](#event:controlchange-sostenuto), [`controlchange-soundvariation`](#event:controlchange-soundvariation), [`controlchange-vibratodelay`](#event:controlchange-vibratodelay), [`controlchange-vibratodepth`](#event:controlchange-vibratodepth), [`controlchange-vibratorate`](#event:controlchange-vibratorate), [`controlchange-volumecoarse`](#event:controlchange-volumecoarse), [`event`](#event:event), [`keyaftertouch`](#event:keyaftertouch), [`localcontrol`](#event:localcontrol), [`midimessage`](#event:midimessage), [`monomode`](#event:monomode), [`noteoff`](#event:noteoff), [`noteon`](#event:noteon), [`nrpn`](#event:nrpn), [`nrpn-datadecrement`](#event:nrpn-datadecrement), [`nrpn-dataentrycoarse`](#event:nrpn-dataentrycoarse), [`nrpn-dataentryfine`](#event:nrpn-dataentryfine), [`nrpn-dataincrement`](#event:nrpn-dataincrement), [`omnimode`](#event:omnimode), [`pitchbend`](#event:pitchbend), [`programchange`](#event:programchange), [`resetallcontrollers`](#event:resetallcontrollers), [`rpn`](#event:rpn), [`rpn-datadecrement`](#event:rpn-datadecrement), [`rpn-dataentrycoarse`](#event:rpn-dataentrycoarse), [`rpn-dataentryfine`](#event:rpn-dataentryfine), [`rpn-dataincrement`](#event:rpn-dataincrement), [`unknownmessage`](#event:unknownmessage) ### `Constructor` Creates an `InputChannel` object. **Parameters** > `new InputChannel(input, number)`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`input`** | Input
||The [`Input`](Input) object this channel belongs to.| |**`number`** | number
||The channel's MIDI number (1-16).|
*** ## Properties ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. ### `.input` {#input} **Since**: 3.0
**Type**: Input
The [`Input`](Input) this channel belongs to. ### `.notesState` {#notesState} **Type**: Array.<boolean>
Contains the current playing state of all MIDI notes of this channel (0-127). The state is `true` for a currently playing note and `false` otherwise. ### `.number` {#number} **Since**: 3.0
**Type**: number
This channel's MIDI number (1-16). ### `.octaveOffset` {#octaveOffset} **Since**: 3.0
**Type**: number
An integer to offset the reported octave of incoming note-specific messages (`noteon`, `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th octave (C4). If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3. Note that this value is combined with the global offset value defined by [`WebMidi.octaveOffset`](WebMidi#octaveOffset) object and with the value defined on the parent input object with [`Input.octaveOffset`](Input#octaveOffset). ### `.parameterNumberEventsEnabled` {#parameterNumberEventsEnabled} **Type**: boolean
Indicates whether events for **Registered Parameter Number** and **Non-Registered Parameter Number** should be dispatched. RPNs and NRPNs are composed of a sequence of specific **control change** messages. When a valid sequence of such control change messages is received, an [`rpn`](#event-rpn) or [`nrpn`](#event-nrpn) event will fire. If an invalid or out-of-order **control change** message is received, it will fall through the collector logic and all buffered **control change** messages will be discarded as incomplete. *** ## Methods ### `.addListener(...)` {#addListener} Adds a listener for the specified event. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to.| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs.| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The value of `this` in the callback function.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time listener for the specified event. The listener will be executed once and then destroyed. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addOneTimeListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The context to invoke the callback function in.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.destroy()` {#destroy} Destroys the `InputChannel` by removing all listeners and severing the link with the MIDI subsystem's input. ### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.getNoteState(...)` {#getNoteState} **Since**: version 3.0.0
Returns the playing status of the specified note (`true` if the note is currently playing, `false` if it is not). The `note` parameter can be an unsigned integer (0-127), a note identifier (`"C4"`, `"G#5"`, etc.) or a [`Note`](Note) object. IF the note is specified using an integer (0-127), no octave offset will be applied. **Parameters** > Signature: `getNoteState(note)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
string
Note
||The note to get the state for. The [`octaveOffset`](#octaveOffset) (channel, input and global) will be factored in for note identifiers and [`Note`](Note) objects.|
**Return Value** > Returns: `boolean`
### `.hasListener(...)` {#hasListener} Returns `true` if the specified event has at least one registered listener. If no event is specified, the method returns `true` if any event has at least one listener registered (this includes global listeners registered to [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). Note: to specifically check for global listeners added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT), use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `hasListener([event], [callback])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
Symbol
|(any event)|The event to check| |[**`callback`**] | function
Listener
|(any callback)|The actual function that was added to the event or the [Listener](Listener) object returned by `addListener()`.|
**Return Value** > Returns: `boolean`
### `.removeListener(...)` {#removeListener} Removes all the listeners that were added to the object upon which the method is called and that match the specified criterias. If no parameters are passed, all listeners added to this object will be removed. If only the `event` parameter is passed, all listeners for that event will be removed from that object. You can remove global listeners by using [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. To use more granular options, you must at least define the `event`. Then, you can specify the callback to match or one or more of the additional options. **Parameters** > Signature: `removeListener([event], [callback], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
||The event name.| |[**`callback`**] | EventEmitter~callback
||Only remove the listeners that match this exact callback function.| |[**`options`**] | Object
||| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
*** ## Events ### `allnotesoff` {#event-allnotesoff} Event emitted when an "all notes off" channel-mode MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`allnotesoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| ### `allsoundoff` {#event-allsoundoff} Event emitted when an "all sound off" channel-mode MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`allsoundoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| ### `channelaftertouch` {#event-channelaftertouch} Event emitted when a control change MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`channelaftertouch`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The raw MIDI value expressed as an integer between 0 and 127.| ### `controlchange` {#event-controlchange} Event emitted when a **control change** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange`| |**`subtype`** |string|The type of control change message that was received.| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-allnotesoff` {#event-controlchange-allnotesoff} Event emitted when a **controlchange-allnotesoff** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-allnotesoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-allsoundoff` {#event-controlchange-allsoundoff} Event emitted when a **controlchange-allsoundoff** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-allsoundoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-attacktime` {#event-controlchange-attacktime} Event emitted when a **controlchange-attacktime** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-attacktime`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-balancecoarse` {#event-controlchange-balancecoarse} Event emitted when a **controlchange-balancecoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-balancecoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-balancefine` {#event-controlchange-balancefine} Event emitted when a **controlchange-balancefine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-balancefine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-bankselectcoarse` {#event-controlchange-bankselectcoarse} Event emitted when a **controlchange-bankselectcoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-bankselectcoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-bankselectfine` {#event-controlchange-bankselectfine} Event emitted when a **controlchange-bankselectfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-bankselectfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-breathcontrollercoarse` {#event-controlchange-breathcontrollercoarse} Event emitted when a **controlchange-breathcontrollercoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-breathcontrollercoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-breathcontrollerfine` {#event-controlchange-breathcontrollerfine} Event emitted when a **controlchange-breathcontrollerfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-breathcontrollerfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-brightness` {#event-controlchange-brightness} Event emitted when a **controlchange-brightness** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-brightness`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-channelvolumefine` {#event-controlchange-channelvolumefine} Event emitted when a **controlchange-channelvolumefine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-channelvolumefine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-controllerxxx` {#event-controlchange-controllerxxx} Event emitted when a **control change** MIDI message has been received and that message is targeting the controller numbered "xxx". Of course, "xxx" should be replaced by a valid controller number (0-127). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-controllerxxx`| |**`subtype`** |string|The type of control change message that was received.| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-damperpedal` {#event-controlchange-damperpedal} Event emitted when a **controlchange-damperpedal** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-damperpedal`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-datadecrement` {#event-controlchange-datadecrement} Event emitted when a **controlchange-datadecrement** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-datadecrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-dataentrycoarse` {#event-controlchange-dataentrycoarse} Event emitted when a **controlchange-dataentrycoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-dataentrycoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-dataentryfine` {#event-controlchange-dataentryfine} Event emitted when a **controlchange-dataentryfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-dataentryfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-dataincrement` {#event-controlchange-dataincrement} Event emitted when a **controlchange-dataincrement** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-dataincrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-decaytime` {#event-controlchange-decaytime} Event emitted when a **controlchange-decaytime** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-decaytime`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effect1depth` {#event-controlchange-effect1depth} Event emitted when a **controlchange-effect1depth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effect1depth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effect2depth` {#event-controlchange-effect2depth} Event emitted when a **controlchange-effect2depth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effect2depth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effect3depth` {#event-controlchange-effect3depth} Event emitted when a **controlchange-effect3depth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effect3depth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effect4depth` {#event-controlchange-effect4depth} Event emitted when a **controlchange-effect4depth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effect4depth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effect5depth` {#event-controlchange-effect5depth} Event emitted when a **controlchange-effect5depth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effect5depth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effectcontrol1coarse` {#event-controlchange-effectcontrol1coarse} Event emitted when a **controlchange-effectcontrol1coarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effectcontrol1coarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effectcontrol1fine` {#event-controlchange-effectcontrol1fine} Event emitted when a **controlchange-effectcontrol1fine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effectcontrol1fine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effectcontrol2coarse` {#event-controlchange-effectcontrol2coarse} Event emitted when a **controlchange-effectcontrol2coarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effectcontrol2coarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-effectcontrol2fine` {#event-controlchange-effectcontrol2fine} Event emitted when a **controlchange-effectcontrol2fine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-effectcontrol2fine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-expressioncoarse` {#event-controlchange-expressioncoarse} Event emitted when a **controlchange-expressioncoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-expressioncoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-expressionfine` {#event-controlchange-expressionfine} Event emitted when a **controlchange-expressionfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-expressionfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-footcontrollercoarse` {#event-controlchange-footcontrollercoarse} Event emitted when a **controlchange-footcontrollercoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-footcontrollercoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-footcontrollerfine` {#event-controlchange-footcontrollerfine} Event emitted when a **controlchange-footcontrollerfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-footcontrollerfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller1` {#event-controlchange-generalpurposecontroller1} Event emitted when a **controlchange-generalpurposecontroller1** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller1`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller2` {#event-controlchange-generalpurposecontroller2} Event emitted when a **controlchange-generalpurposecontroller2** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller2`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller3` {#event-controlchange-generalpurposecontroller3} Event emitted when a **controlchange-generalpurposecontroller3** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller3`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller4` {#event-controlchange-generalpurposecontroller4} Event emitted when a **controlchange-generalpurposecontroller4** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller4`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller5` {#event-controlchange-generalpurposecontroller5} Event emitted when a **controlchange-generalpurposecontroller5** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller5`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller6` {#event-controlchange-generalpurposecontroller6} Event emitted when a **controlchange-generalpurposecontroller6** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller6`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller7` {#event-controlchange-generalpurposecontroller7} Event emitted when a **controlchange-generalpurposecontroller7** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller7`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-generalpurposecontroller8` {#event-controlchange-generalpurposecontroller8} Event emitted when a **controlchange-generalpurposecontroller8** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-generalpurposecontroller8`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-highresolutionvelocityprefix` {#event-controlchange-highresolutionvelocityprefix} Event emitted when a **controlchange-highresolutionvelocityprefix** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-highresolutionvelocityprefix`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-hold2` {#event-controlchange-hold2} Event emitted when a **controlchange-hold2** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-hold2`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-legatopedal` {#event-controlchange-legatopedal} Event emitted when a **controlchange-legatopedal** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-legatopedal`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-localcontrol` {#event-controlchange-localcontrol} Event emitted when a **controlchange-localcontrol** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-localcontrol`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-modulationwheelcoarse` {#event-controlchange-modulationwheelcoarse} Event emitted when a **controlchange-modulationwheelcoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-modulationwheelcoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-modulationwheelfine` {#event-controlchange-modulationwheelfine} Event emitted when a **controlchange-modulationwheelfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-modulationwheelfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-monomodeon` {#event-controlchange-monomodeon} Event emitted when a **controlchange-monomodeon** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-monomodeon`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-nonregisteredparametercoarse` {#event-controlchange-nonregisteredparametercoarse} Event emitted when a **controlchange-nonregisteredparametercoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-nonregisteredparametercoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-nonregisteredparameterfine` {#event-controlchange-nonregisteredparameterfine} Event emitted when a **controlchange-nonregisteredparameterfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-nonregisteredparameterfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-omnimodeoff` {#event-controlchange-omnimodeoff} Event emitted when a **controlchange-omnimodeoff** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-omnimodeoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-omnimodeon` {#event-controlchange-omnimodeon} Event emitted when a **controlchange-omnimodeon** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-omnimodeon`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-pancoarse` {#event-controlchange-pancoarse} Event emitted when a **controlchange-pancoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-pancoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-panfine` {#event-controlchange-panfine} Event emitted when a **controlchange-panfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-panfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-polymodeon` {#event-controlchange-polymodeon} Event emitted when a **controlchange-polymodeon** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-polymodeon`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-portamento` {#event-controlchange-portamento} Event emitted when a **controlchange-portamento** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-portamento`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-portamentocontrol` {#event-controlchange-portamentocontrol} Event emitted when a **controlchange-portamentocontrol** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-portamentocontrol`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-portamentotimecoarse` {#event-controlchange-portamentotimecoarse} Event emitted when a **controlchange-portamentotimecoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-portamentotimecoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-portamentotimefine` {#event-controlchange-portamentotimefine} Event emitted when a **controlchange-portamentotimefine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-portamentotimefine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-registeredparametercoarse` {#event-controlchange-registeredparametercoarse} Event emitted when a **controlchange-registeredparametercoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-registeredparametercoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-registeredparameterfine` {#event-controlchange-registeredparameterfine} Event emitted when a **controlchange-registeredparameterfine** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-registeredparameterfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-releasetime` {#event-controlchange-releasetime} Event emitted when a **controlchange-releasetime** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-releasetime`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-resetallcontrollers` {#event-controlchange-resetallcontrollers} Event emitted when a **controlchange-resetallcontrollers** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-resetallcontrollers`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-resonance` {#event-controlchange-resonance} Event emitted when a **controlchange-resonance** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-resonance`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-softpedal` {#event-controlchange-softpedal} Event emitted when a **controlchange-softpedal** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-softpedal`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-sostenuto` {#event-controlchange-sostenuto} Event emitted when a **controlchange-sostenuto** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-sostenuto`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-soundvariation` {#event-controlchange-soundvariation} Event emitted when a **controlchange-soundvariation** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-soundvariation`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-vibratodelay` {#event-controlchange-vibratodelay} Event emitted when a **controlchange-vibratodelay** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-vibratodelay`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-vibratodepth` {#event-controlchange-vibratodepth} Event emitted when a **controlchange-vibratodepth** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-vibratodepth`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-vibratorate` {#event-controlchange-vibratorate} Event emitted when a **controlchange-vibratorate** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-vibratorate`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `controlchange-volumecoarse` {#event-controlchange-volumecoarse} Event emitted when a **controlchange-volumecoarse** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`controlchange-volumecoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`controller`** |object|| |**`controller.number`** |object|The number of the controller.| |**`controller.name`** |object|The usual name or function of the controller.| |**`controller.description`** |object|A user-friendly representation of the controller's default function| |**`controller.position`** |string|Whether the controller is meant to be an `msb` or `lsb`| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The value expressed as an integer (between 0 and 127).| ### `keyaftertouch` {#event-keyaftertouch} Event emitted when a **key-specific aftertouch** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`"keyaftertouch"`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`note`** |object|A [`Note`](Note) object containing information such as note name and number.| |**`value`** |number|The aftertouch amount expressed as a float between 0 and 1.| |**`rawValue`** |number|The aftertouch amount expressed as an integer (between 0 and 127).| ### `localcontrol` {#event-localcontrol} Event emitted when a "local control" channel-mode MIDI message has been received. The value property of the event is set to either `true` (local control on) of `false` (local control off). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`localcontrol`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |boolean|For local control on, the value is `true`. For local control off, the value is `false`.| |**`rawValue`** |boolean|For local control on, the value is `127`. For local control off, the value is `0`.| ### `midimessage` {#event-midimessage} Event emitted when a MIDI message of any kind is received by an `InputChannel` **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`midimessage`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| ### `monomode` {#event-monomode} Event emitted when a "mono/poly mode" MIDI message has been received. The value property of the event is set to either `true` (mono mode on / poly mode off) or `false` (mono mode off / poly mode on). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`monomode`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |boolean|The value is `true` for omni mode on and false for omni mode off.| |**`rawValue`** |boolean|The raw MIDI value| ### `noteoff` {#event-noteoff} Event emitted when a **note off** MIDI message has been received on the channel. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`noteoff`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment ([`DOMHighResTimeStamp`](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp)) when the event occurred (in milliseconds since the navigation start of the document).| |**`note`** |object|A [`Note`](Note) object containing information such as note name, octave and release velocity.| |**`value`** |number|The release velocity amount expressed as a float between 0 and 1.| |**`rawValue`** |number|The release velocity amount expressed as an integer (between 0 and 127).| ### `noteon` {#event-noteon} Event emitted when a **note on** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`noteon`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`note`** |object|A [`Note`](Note) object containing information such as note name, octave and release velocity.| |**`value`** |number|The attack velocity amount expressed as a float between 0 and 1.| |**`rawValue`** |number|The attack velocity amount expressed as an integer (between 0 and 127).| ### `nrpn` {#event-nrpn} Event emitted when any NRPN message is received on the input. There are four subtypes of NRPN messages: * `nrpn-dataentrycoarse` * `nrpn-dataentryfine` * `nrpn-dataincrement` * `nrpn-datadecrement` The parameter to which the message applies can be found in the event's `parameter` property. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`nrpn`| |**`subtype`** |string|The precise type of NRPN message that was received.| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |number|The non-registered parameter number (0-16383)| |**`parameterMsb`** |number|The MSB portion of the non-registered parameter number (0-127)| |**`parameterLsb:`** |number|The LSB portion of the non-registered parameter number (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `nrpn-datadecrement` {#event-nrpn-datadecrement} Event emitted when an **NRPN data decrement** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`nrpn-datadecrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `nrpn-dataentrycoarse` {#event-nrpn-dataentrycoarse} Event emitted when an **NRPN data entry coarse** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`nrpn-dataentrycoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `nrpn-dataentryfine` {#event-nrpn-dataentryfine} Event emitted when an **NRPN data entry fine** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`nrpn-dataentryfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `nrpn-dataincrement` {#event-nrpn-dataincrement} Event emitted when an **NRPN data increment** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`nrpn-dataincrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `omnimode` {#event-omnimode} Event emitted when an "omni mode" channel-mode MIDI message has been received. The value property of the event is set to either `true` (omni mode on) of `false` (omni mode off). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`"omnimode"`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |boolean|The value is `true` for omni mode on and false for omni mode off.| |**`rawValue`** |boolean|The raw MIDI value| ### `pitchbend` {#event-pitchbend} Event emitted when a pitch bend MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`pitchbend`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |number|The value expressed as a float between 0 and 1.| |**`rawValue`** |number|The raw MIDI value expressed as an integer (between 0 and 16383).| ### `programchange` {#event-programchange} Event emitted when a **program change** MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`programchange`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`value`** |number|The value expressed as an integer between 0 and 127.| |**`rawValue`** |number|The raw MIDI value expressed as an integer between 0 and 127.| ### `resetallcontrollers` {#event-resetallcontrollers} Event emitted when a "reset all controllers" channel-mode MIDI message has been received. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| ### `rpn` {#event-rpn} Event emitted when any RPN message is received on the input. There are four subtypes of RPN messages: * `rpn-dataentrycoarse` * `rpn-dataentryfine` * `rpn-dataincrement` * `rpn-datadecrement` The parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`rpn`| |**`subtype`** |string|The precise type of RPN message that was received.| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `rpn-datadecrement` {#event-rpn-datadecrement} Event emitted when an **RPN data decrement** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`rpn-datadecrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `rpn-dataentrycoarse` {#event-rpn-dataentrycoarse} Event emitted when an **RPN data entry coarse** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`rpn-dataentrycoarse`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `rpn-dataentryfine` {#event-rpn-dataentryfine} Event emitted when an **RPN data entry fine** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`EnumerationsREGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`rpn-dataentryfine`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ### `rpn-dataincrement` {#event-rpn-dataincrement} Event emitted when an **RPN data increment** message is received on the input. The specific parameter to which the message applies can be found in the event's `parameter` property. It is one of the ones defined in [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`type`** |string|`rpn-dataincrement`| |**`target`** |InputChannel|The object that dispatched the event.| |**`port`** |Input|The `Input` that triggered the event.| |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`message`** |Message|A [`Message`](Message) object containing information about the incoming MIDI message.| |**`parameter`** |string|The registered parameter's name| |**`parameterMsb`** |number|The MSB portion of the registered parameter (0-127)| |**`parameterLsb:`** |number|The LSB portion of the registered parameter (0-127)| |**`value`** |number|The received value as a normalized number between 0 and 1.| |**`rawValue`** |number|The value as received (0-127)| ================================================ FILE: website/api/classes/Listener.md ================================================ # Listener The `Listener` class represents a single event listener object. Such objects keep all relevant contextual information such as the event being listened to, the object the listener was attached to, the callback function and so on. ### `Constructor` Creates a new `Listener` object **Parameters** > `new Listener(event, target, callback, [options])`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event being listened to| |**`target`** | EventEmitter
||The [`EventEmitter`](EventEmitter) object that the listener is attached to.| |**`callback`** | EventEmitter~callback
||The function to call when the listener is triggered| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|target|The context to invoke the listener in (a.k.a. the value of `this` inside the callback function).| |[**`options.remaining`**] | number
|Infinity|The remaining number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments that will be passed separately to the callback function upon execution. The array is stored in the [`arguments`](#arguments) property and can be retrieved or modified as desired.|
**Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `ReferenceError` : The `target` parameter is mandatory. * `TypeError` : The `callback` must be a function. *** ## Properties ### `.arguments` {#arguments} **Type**: array
An array of arguments to pass to the callback function upon execution. ### `.callback` {#callback} **Type**: function
The callback function to execute. ### `.context` {#context} **Type**: Object
The context to execute the callback function in (a.k.a. the value of `this` inside the callback function) ### `.count` {#count} **Type**: number
The number of times the listener function was executed. ### `.event` {#event} **Type**: string
The event name. ### `.remaining` {#remaining} **Type**: number
The remaining number of times after which the callback should automatically be removed. ### `.suspended` {#suspended} **Type**: boolean
Whether this listener is currently suspended or not. ### `.target` {#target} **Type**: EventEmitter
The object that the event is attached to (or that emitted the event). *** ## Methods ### `.remove()` {#remove} Removes the listener from its target. ================================================ FILE: website/api/classes/Message.md ================================================ # Message The `Message` class represents a single MIDI message. It has several properties that make it easy to make sense of the binary data it contains. **Since**: 3.0.0 ### `Constructor` Creates a new `Message` object from raw MIDI data. **Parameters** > `new Message(data)`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`data`** | Uint8Array
||The raw data of the MIDI message as a [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of integers between `0` and `255`.|
*** ## Properties ### `.channel` {#channel} **Type**: number
**Attributes**: read-only
The MIDI channel number (`1` - `16`) that the message is targeting. This is only for channel-specific messages. For system messages, this will be left `undefined`. ### `.command` {#command} **Type**: number
**Attributes**: read-only
An integer identifying the MIDI command. For channel-specific messages, the value is 4-bit and will be between `8` and `14`. For system messages, the value will be between `240` and `255`. ### `.data` {#data} **Type**: Array.<number>
**Attributes**: read-only
An array containing all the bytes of the MIDI message. Each byte is an integer between `0` and `255`. ### `.dataBytes` {#dataBytes} **Type**: Array.<number>
**Attributes**: read-only
An array of the the data byte(s) of the MIDI message (as opposed to the status byte). When the message is a system exclusive message (sysex), `dataBytes` explicitly excludes the manufacturer ID and the sysex end byte so only the actual data is included. ### `.isChannelMessage` {#isChannelMessage} **Type**: boolean
**Attributes**: read-only
A boolean indicating whether the MIDI message is a channel-specific message. ### `.isSystemMessage` {#isSystemMessage} **Type**: boolean
**Attributes**: read-only
A boolean indicating whether the MIDI message is a system message (not specific to a channel). ### `.manufacturerId` {#manufacturerId} **Type**: Array.<number>
**Attributes**: read-only
When the message is a system exclusive message (sysex), this property contains an array with either 1 or 3 entries that identify the manufacturer targeted by the message. To know how to translate these entries into manufacturer names, check out the official list: https://www.midi.org/specifications-old/item/manufacturer-id-numbers ### `.rawData` {#rawData} **Type**: Uint8Array
**Attributes**: read-only
A [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) containing the bytes of the MIDI message. Each byte is an integer between `0` and `255`. ### `.rawDataBytes` {#rawDataBytes} **Type**: Uint8Array
**Attributes**: read-only
A [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of the data byte(s) of the MIDI message. When the message is a system exclusive message (sysex), `rawDataBytes` explicitly excludes the manufacturer ID and the sysex end byte so only the actual data is included. ### `.statusByte` {#statusByte} **Type**: number
**Attributes**: read-only
The MIDI status byte of the message as an integer between `0` and `255`. ### `.type` {#type} **Type**: string
**Attributes**: read-only
The type of message as a string (`"noteon"`, `"controlchange"`, `"sysex"`, etc.) ================================================ FILE: website/api/classes/Note.md ================================================ # Note The `Note` class represents a single musical note such as `"D3"`, `"G#4"`, `"F-1"`, `"Gb7"`, etc. `Note` objects can be played back on a single channel by calling [`OutputChannel.playNote()`](OutputChannel#playNote) or, on multiple channels of the same output, by calling [`Output.playNote()`](Output#playNote). The note has [`attack`](#attack) and [`release`](#release) velocities set at `0.5` by default. These can be changed by passing in the appropriate option. It is also possible to set a system-wide default for attack and release velocities by using the [`WebMidi.defaults`](WebMidi#defaults) property. If you prefer to work with raw MIDI values (`0` to `127`), you can use [`rawAttack`](#rawAttack) and [`rawRelease`](#rawRelease) to both get and set the values. The note may have a [`duration`](#duration). If it does, playback will be automatically stopped when the duration has elapsed by sending a `"noteoff"` event. By default, the duration is set to `Infinity`. In this case, it will never stop playing unless explicitly stopped by calling a method such as [`OutputChannel.stopNote()`](OutputChannel#stopNote), [`Output.stopNote()`](Output#stopNote) or similar. **Since**: 3.0.0 ### `Constructor` Creates a `Note` object. **Parameters** > `new Note(value, [options])`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | string
number
||The value used to create the note. If an identifier string is used, it must start with the note letter, optionally followed by an accidental and followed by the octave number (`"C3"`, `"G#4"`, `"F-1"`, `"Db7"`, etc.). If a number is used, it must be an integer between 0 and 127. In this case, middle C is considered to be C4 (note number 60).| |[**`options`**] | object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the note should be explicitly stopped.| |[**`options.attack`**] | number
|0.5|The note's attack velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.release`**] | number
|0.5|The note's release velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.| |[**`options.rawAttack`**] | number
|64|The note's attack velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.rawRelease`**] | number
|64|The note's release velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.|
**Throws**: * `Error` : Invalid note identifier * `RangeError` : Invalid name value * `RangeError` : Invalid accidental value * `RangeError` : Invalid octave value * `RangeError` : Invalid duration value * `RangeError` : Invalid attack value * `RangeError` : Invalid release value *** ## Properties ### `.accidental` {#accidental} **Since**: 3.0.0
**Type**: string
The accidental (#, ##, b or bb) of the note. ### `.attack` {#attack} **Since**: 3.0.0
**Type**: number
The attack velocity of the note as a float between 0 and 1. ### `.duration` {#duration} **Since**: 3.0.0
**Type**: number
The duration of the note as a positive decimal number representing the number of milliseconds that the note should play for. ### `.identifier` {#identifier} **Since**: 3.0.0
**Type**: string
The name, optional accidental and octave of the note, as a string. ### `.name` {#name} **Since**: 3.0.0
**Type**: string
The name (letter) of the note. If you need the full name with octave and accidental, you can use the [`identifier`](#Note+identifier) property instead. ### `.number` {#number} **Since**: 3.0.0
**Type**: number
**Attributes**: read-only
The MIDI number of the note (`0` - `127`). This number is derived from the note identifier using C4 as a reference for middle C. ### `.octave` {#octave} **Since**: 3.0.0
**Type**: number
The octave of the note. ### `.rawAttack` {#rawAttack} **Since**: 3.0.0
**Type**: number
The attack velocity of the note as a positive integer between 0 and 127. ### `.rawRelease` {#rawRelease} **Since**: 3.0.0
**Type**: number
The release velocity of the note as a positive integer between 0 and 127. ### `.release` {#release} **Since**: 3.0.0
**Type**: number
The release velocity of the note as an integer between 0 and 1. *** ## Methods ### `.getOffsetNumber(...)` {#getOffsetNumber} Returns a MIDI note number offset by octave and/or semitone. If the calculated value is less than 0, 0 will be returned. If the calculated value is more than 127, 127 will be returned. If an invalid value is supplied, 0 will be used. **Parameters** > Signature: `getOffsetNumber([octaveOffset], [semitoneOffset])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`octaveOffset`**] | number
|0|An integer to offset the note number by octave.| |[**`semitoneOffset`**] | number
|0|An integer to offset the note number by semitone.|
**Return Value** > Returns: `number`
An integer between 0 and 127 ================================================ FILE: website/api/classes/Output.md ================================================ # Output The `Output` class represents a single MIDI output port (not to be confused with a MIDI channel). A port is made available by a MIDI device. A MIDI device can advertise several input and output ports. Each port has 16 MIDI channels which can be accessed via the [`channels`](#channels) property. The `Output` object is automatically instantiated by the library according to the host's MIDI subsystem and should not be directly instantiated. You can access all available `Output` objects by referring to the [`WebMidi.outputs`](WebMidi#outputs) array or by using methods such as [`WebMidi.getOutputByName()`](WebMidi#getOutputByName) or [`WebMidi.getOutputById()`](WebMidi#getOutputById). **Extends**: [`EventEmitter`](EventEmitter) **Fires**: [`closed`](#event:closed), [`disconnected`](#event:disconnected), [`opened`](#event:opened) ### `Constructor` Creates an `Output` object. **Parameters** > `new Output(midiOutput)`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`midiOutput`** | MIDIOutput
||[`MIDIOutput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIOutput) object as provided by the MIDI subsystem.|
*** ## Properties ### `.channels` {#channels} **Type**: Array.<OutputChannel>
Array containing the 16 [`OutputChannel`](OutputChannel) objects available provided by this `Output`. The channels are numbered 1 through 16. ### `.connection` {#connection} **Type**: string
**Attributes**: read-only
Output port's connection state: `pending`, `open` or `closed`. ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. ### `.id` {#id} **Type**: string
**Attributes**: read-only
ID string of the MIDI output. The ID is host-specific. Do not expect the same ID on different platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for the same port. ### `.manufacturer` {#manufacturer} **Type**: string
**Attributes**: read-only
Name of the manufacturer of the device that makes this output port available. ### `.name` {#name} **Type**: string
**Attributes**: read-only
Name of the MIDI output. ### `.octaveOffset` {#octaveOffset} **Since**: 3.0
**Type**: number
An integer to offset the octave of outgoing notes. By default, middle C (MIDI note number 60) is placed on the 4th octave (C4). Note that this value is combined with the global offset value defined in [`WebMidi.octaveOffset`](WebMidi#octaveOffset) (if any). ### `.state` {#state} **Type**: string
**Attributes**: read-only
State of the output port: `connected` or `disconnected`. ### `.type` {#type} **Type**: string
**Attributes**: read-only
Type of the output port (it will always be: `output`). *** ## Methods ### `.addListener(...)` {#addListener} Adds a listener for the specified event. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to.| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs.| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The value of `this` in the callback function.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time listener for the specified event. The listener will be executed once and then destroyed. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addOneTimeListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The context to invoke the callback function in.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.clear()` {#clear} Clears all MIDI messages that have been queued and scheduled but not yet sent. **Warning**: this method is defined in the [Web MIDI API specification](https://www.w3.org/TR/webmidi/#MIDIOutput) but has not been implemented by all browsers yet. You can follow [this issue](https://github.com/djipco/webmidi/issues/52) for more info. **Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.close()` {#close} **Attributes**: async Closes the output connection. When an output is closed, it cannot be used to send MIDI messages until the output is opened again by calling [`open()`](#open). You can check the connection status by looking at the [`connection`](#connection) property. **Return Value** > Returns: `Promise.`
### `.destroy()` {#destroy} **Attributes**: async Destroys the `Output`. All listeners are removed, all channels are destroyed and the MIDI subsystem is unlinked. **Return Value** > Returns: `Promise.`
### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.hasListener(...)` {#hasListener} Returns `true` if the specified event has at least one registered listener. If no event is specified, the method returns `true` if any event has at least one listener registered (this includes global listeners registered to [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). Note: to specifically check for global listeners added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT), use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `hasListener([event], [callback])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
Symbol
|(any event)|The event to check| |[**`callback`**] | function
Listener
|(any callback)|The actual function that was added to the event or the [Listener](Listener) object returned by `addListener()`.|
**Return Value** > Returns: `boolean`
### `.open()` {#open} **Attributes**: async Opens the output for usage. When the library is enabled, all ports are automatically opened. This method is only useful for ports that have been manually closed. **Return Value** > Returns: `Promise.`
The promise is fulfilled with the `Output` object. ### `.playNote(...)` {#playNote} Plays a note or an array of notes on one or more channels of this output. If you intend to play notes on a single channel, you should probably use [`OutputChannel.playNote()`](OutputChannel#playNote) instead. The first parameter is the note to play. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The `playNote()` method sends a **note on** MIDI message for all specified notes on all specified channels. If no channel is specified, it will send to all channels. If a `duration` is set in the `options` parameter or in the [`Note`](Note) object's [`duration`](Note#duration) property, it will also schedule a **note off** message to end the note after said duration. If no `duration` is set, the note will simply play until a matching **note off** message is sent with [`stopNote()`](#stopNote). The execution of the **note on** command can be delayed by using the `time` property of the `options` parameter. When using [`Note`](Note) objects, the durations and velocities defined in the [`Note`](Note) objects have precedence over the ones specified via the method's `options` parameter. **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is functionally equivalent to a **note off** message. **Parameters** > Signature: `playNote(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
string
Note
Array.<number>
Array.<string>
Array.<Note>
||The note(s) to play. The notes can be specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a [`Note`](Note) object or an array of the previous types. When using a note identifier, octave range must be between -1 and 9. The lowest note is C-1 (MIDI note number `0`) and the highest note is G9 (MIDI note number `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.duration`**] | number
||The number of milliseconds after which a **note off** message will be scheduled. If left undefined, only a **note on** message is sent.| |[**`options.attack`**] | number
|0.5|The velocity at which to play the note (between `0` and `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawAttack`**] | number
|64|The attack velocity at which to play the note (between `0` and `127`). This has priority over the `attack` property. An invalid velocity value will silently trigger the default of 64.| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid velocity value will silently trigger the default of `0.5`. This is only used with the **note off** event triggered when `options.duration` is set.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). This has priority over the `release` property. An invalid velocity value will silently trigger the default of 64. This is only used with the **note off** event triggered when `options.duration` is set.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.removeListener(...)` {#removeListener} Removes all the listeners that were added to the object upon which the method is called and that match the specified criterias. If no parameters are passed, all listeners added to this object will be removed. If only the `event` parameter is passed, all listeners for that event will be removed from that object. You can remove global listeners by using [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. To use more granular options, you must at least define the `event`. Then, you can specify the callback to match or one or more of the additional options. **Parameters** > Signature: `removeListener([event], [callback], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
||The event name.| |[**`callback`**] | EventEmitter~callback
||Only remove the listeners that match this exact callback function.| |[**`options`**] | Object
||| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.send(...)` {#send} Sends a MIDI message on the MIDI output port. If no time is specified, the message will be sent immediately. The message should be an array of 8 bit unsigned integers (0-225), a [`Uint8Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) object or a [`Message`](Message) object. It is usually not necessary to use this method directly as you can use one of the simpler helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), [`sendControlChange()`](#sendControlChange), etc. Details on the format of MIDI messages are available in the summary of [MIDI messages](https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message) from the MIDI Manufacturers Association. **Parameters** > Signature: `send(message, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`message`** | Array.<number>
Uint8Array
Message
||An array of 8bit unsigned integers, a `Uint8Array` object (not available in Node.js) containing the message bytes or a `Message` object.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The first byte (status) must be an integer between 128 and 255. ### `.sendActiveSensing(...)` {#sendActiveSensing} Sends an **active sensing** real-time message. This tells the device connected to this port that the connection is still good. Active sensing messages are often sent every 300 ms if there was no other activity on the MIDI port. **Parameters** > Signature: `sendActiveSensing([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendAllNotesOff(...)` {#sendAllNotesOff} **Since**: 3.0.0
Sends an **all notes off** channel mode message. This will make all currently playing notes fade out just as if their key had been released. This is different from the [`sendAllSoundOff()`](#sendAllSoundOff) method which mutes all sounds immediately. **Parameters** > Signature: `sendAllNotesOff([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
### `.sendAllSoundOff(...)` {#sendAllSoundOff} **Since**: 3.0.0
Sends an **all sound off** channel mode message. This will silence all sounds playing on that channel but will not prevent new sounds from being triggered. **Parameters** > Signature: `sendAllSoundOff([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
### `.sendChannelAftertouch(...)` {#sendChannelAftertouch} **Since**: 3.0.0
Sends a MIDI **channel aftertouch** message to the specified channel(s). For key-specific aftertouch, you should instead use [`setKeyAftertouch()`](#setKeyAftertouch). **Parameters** > Signature: `sendChannelAftertouch([pressure], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`pressure`**] | number
|0.5|The pressure level (between `0` and `1`). An invalid pressure value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the pressure can be defined by using an integer between `0` and `127`.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendChannelMode(...)` {#sendChannelMode} Sends a MIDI **channel mode** message to the specified channel(s). The channel mode message to send can be specified numerically or by using one of the following common names: | Type |Number| Shortcut Method | | ---------------------|------|-------------------------------------------------------------- | | `allsoundoff` | 120 | [`sendAllSoundOff()`](#sendAllSoundOff) | | `resetallcontrollers`| 121 | [`sendResetAllControllers()`](#sendResetAllControllers) | | `localcontrol` | 122 | [`sendLocalControl()`](#sendLocalControl) | | `allnotesoff` | 123 | [`sendAllNotesOff()`](#sendAllNotesOff) | | `omnimodeoff` | 124 | [`sendOmniMode(false)`](#sendOmniMode) | | `omnimodeon` | 125 | [`sendOmniMode(true)`](#sendOmniMode) | | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`](#sendPolyphonicMode) | | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`](#sendPolyphonicMode) | Note: as you can see above, to make it easier, all channel mode messages also have a matching helper method. It should also be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` may require a value that's not zero. For that reason, the `value` parameter is optional and defaults to 0. **Parameters** > Signature: `sendChannelMode(command, [value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`command`** | number
string
||The numerical identifier of the channel mode message (integer between 120-127) or its name as a string.| |[**`value`**] | number
|0|The value to send (integer between 0-127).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `TypeError` : Invalid channel mode message name. * `RangeError` : Channel mode controller numbers must be between 120 and 127. * `RangeError` : Value must be an integer between 0 and 127. ### `.sendClock(...)` {#sendClock} Sends a MIDI **clock** real-time message. According to the standard, there are 24 MIDI clocks for every quarter note. **Parameters** > Signature: `sendClock([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendContinue(...)` {#sendContinue} Sends a **continue** real-time message. This resumes song playback where it was previously stopped or where it was last cued with a song position message. To start playback from the start, use the [`sendStart()`](#Output+sendStart)` method. **Parameters** > Signature: `sendContinue([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendControlChange(...)` {#sendControlChange} Sends a MIDI **control change** message to the specified channel(s) at the scheduled time. The control change message to send can be specified numerically (0-127) or by using one of the following common names: | Number | Name | |--------|-------------------------------| | 0 |`bankselectcoarse` | | 1 |`modulationwheelcoarse` | | 2 |`breathcontrollercoarse` | | 4 |`footcontrollercoarse` | | 5 |`portamentotimecoarse` | | 6 |`dataentrycoarse` | | 7 |`volumecoarse` | | 8 |`balancecoarse` | | 10 |`pancoarse` | | 11 |`expressioncoarse` | | 12 |`effectcontrol1coarse` | | 13 |`effectcontrol2coarse` | | 18 |`generalpurposeslider3` | | 19 |`generalpurposeslider4` | | 32 |`bankselectfine` | | 33 |`modulationwheelfine` | | 34 |`breathcontrollerfine` | | 36 |`footcontrollerfine` | | 37 |`portamentotimefine` | | 38 |`dataentryfine` | | 39 |`volumefine` | | 40 |`balancefine` | | 42 |`panfine` | | 43 |`expressionfine` | | 44 |`effectcontrol1fine` | | 45 |`effectcontrol2fine` | | 64 |`holdpedal` | | 65 |`portamento` | | 66 |`sustenutopedal` | | 67 |`softpedal` | | 68 |`legatopedal` | | 69 |`hold2pedal` | | 70 |`soundvariation` | | 71 |`resonance` | | 72 |`soundreleasetime` | | 73 |`soundattacktime` | | 74 |`brightness` | | 75 |`soundcontrol6` | | 76 |`soundcontrol7` | | 77 |`soundcontrol8` | | 78 |`soundcontrol9` | | 79 |`soundcontrol10` | | 80 |`generalpurposebutton1` | | 81 |`generalpurposebutton2` | | 82 |`generalpurposebutton3` | | 83 |`generalpurposebutton4` | | 91 |`reverblevel` | | 92 |`tremololevel` | | 93 |`choruslevel` | | 94 |`celestelevel` | | 95 |`phaserlevel` | | 96 |`dataincrement` | | 97 |`datadecrement` | | 98 |`nonregisteredparametercoarse` | | 99 |`nonregisteredparameterfine` | | 100 |`registeredparametercoarse` | | 101 |`registeredparameterfine` | | 120 |`allsoundoff` | | 121 |`resetallcontrollers` | | 122 |`localcontrol` | | 123 |`allnotesoff` | | 124 |`omnimodeoff` | | 125 |`omnimodeon` | | 126 |`monomodeon` | | 127 |`polymodeon` | Note: as you can see above, not all control change message have a matching name. This does not mean you cannot use the others. It simply means you will need to use their number (`0` - `127`) instead of their name. While you can still use them, numbers `120` to `127` are usually reserved for *channel mode* messages. See [`sendChannelMode()`](#sendChannelMode) method for more info. To view a list of all available **control change** messages, please consult [Table 3 - Control Change Messages](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) from the MIDI specification. **Parameters** > Signature: `sendControlChange(controller, [value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`controller`** | number
string
||The MIDI controller name or number (0-127).| |[**`value`**] | number
|0|The value to send (0-127).| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : Controller numbers must be between 0 and 127. * `RangeError` : Invalid controller name. ### `.sendKeyAftertouch(...)` {#sendKeyAftertouch} **Since**: 3.0.0
Sends a MIDI **key aftertouch** message to the specified channel(s) at the scheduled time. This is a key-specific aftertouch. For a channel-wide aftertouch message, use [`setChannelAftertouch()`](#setChannelAftertouch). **Parameters** > Signature: `sendKeyAftertouch(note, [pressure], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) for which you are sending an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`pressure`**] | number
|0.5|The pressure level (between 0 and 1). An invalid pressure value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the pressure can be defined by using an integer between 0 and 127.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendLocalControl(...)` {#sendLocalControl} **Since**: 3.0.0
Turns local control on or off. Local control is usually enabled by default. If you disable it, the instrument will no longer trigger its own sounds. It will only send the MIDI messages to its out port. **Parameters** > Signature: `sendLocalControl([state], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`state`**] | boolean
|false|Whether to activate local control (`true`) or disable it (`false`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendMasterTuning(...)` {#sendMasterTuning} **Since**: 3.0.0
Sends a master tuning message to the specified channel(s). The value is decimal and must be larger than `-65` semitones and smaller than `64` semitones. Because of the way the MIDI specification works, the decimal portion of the value will be encoded with a resolution of 14bit. The integer portion must be between -64 and 63 inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and a **Master Fine Tuning** RPN messages. **Parameters** > Signature: `sendMasterTuning([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
|0.0|The desired decimal adjustment value in semitones (-65 < x < 64)| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The value must be a decimal number between larger than -65 and smaller than 64. ### `.sendModulationRange(...)` {#sendModulationRange} **Since**: 3.0.0
Sends a **modulation depth range** message to the specified channel(s) so that they adjust the depth of their modulation wheel's range. The range can be specified with the `semitones` parameter, the `cents` parameter or by specifying both parameters at the same time. **Parameters** > Signature: `sendModulationRange([semitones], [cents], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`semitones`**] | number
|0|The desired adjustment value in semitones (integer between 0 and 127).| |[**`cents`**] | number
|0|The desired adjustment value in cents (integer between 0 and 127).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The msb value must be between 0 and 127 * `RangeError` : The lsb value must be between 0 and 127 ### `.sendNoteOff(...)` {#sendNoteOff} Sends a **note off** message for the specified MIDI note number on the specified channel(s). The first parameter is the note to stop. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The execution of the **note off** command can be delayed by using the `time` property of the `options` parameter. **Parameters** > Signature: `sendNoteOff(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) to stop. The notes can be specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `64`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendNoteOn(...)` {#sendNoteOn} Sends a **note on** message for the specified MIDI note number on the specified channel(s). The first parameter is the number. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The execution of the **note on** command can be delayed by using the `time` property of the `options` parameter. **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is functionally equivalent to a **note off** message. **Parameters** > Signature: `sendNoteOn(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) to stop. The notes can be specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.attack`**] | number
|0.5|The velocity at which to play the note (between `0` and `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawAttack`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid velocity value will silently trigger the default of `64`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendNrpnValue(...)` {#sendNrpnValue} Sets a non-registered parameter to the specified value. The NRPN is selected by passing a two-position array specifying the values of the two control bytes. The value is specified by passing a single integer (most cases) or an array of two integers. NRPNs are not standardized in any way. Each manufacturer is free to implement them any way they see fit. For example, according to the Roland GS specification, you can control the **vibrato rate** using NRPN (`1`, `8`). Therefore, to set the **vibrato rate** value to `123` you would use: ```js WebMidi.outputs[0].sendNrpnValue([1, 8], 123); ``` You probably want to should select a channel so the message is not sent to all channels. For instance, to send to channel `1` of the first output port, you would use: ```js WebMidi.outputs[0].sendNrpnValue([1, 8], 123, 1); ``` In some rarer cases, you need to send two values with your NRPN messages. In such cases, you would use a 2-position array. For example, for its **ClockBPM** parameter (`2`, `63`), Novation uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the value to send was `10`, you could use: ```js WebMidi.outputs[0].sendNrpnValue([2, 63], [0, 10], 1); ``` For further implementation details, refer to the manufacturer's documentation. **Parameters** > Signature: `sendNrpnValue(parameter, [data], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | Array.<number>
||A two-position array specifying the two control bytes (`0x63`, `0x62`) that identify the non-registered parameter.| |[**`data`**] | number
Array.<number>
|[]|An integer or an array of integers with a length of 1 or 2 specifying the desired data.| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The control value must be between 0 and 127. * `RangeError` : The msb value must be between 0 and 127 ### `.sendOmniMode(...)` {#sendOmniMode} **Since**: 3.0.0
Sets OMNI mode to **on** or **off** for the specified channel(s). MIDI's OMNI mode causes the instrument to respond to messages from all channels. It should be noted that support for OMNI mode is not as common as it used to be. **Parameters** > Signature: `sendOmniMode([state], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`state`**] | boolean
||Whether to activate OMNI mode (`true`) or not (`false`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `TypeError` : Invalid channel mode message name. * `RangeError` : Channel mode controller numbers must be between 120 and 127. * `RangeError` : Value must be an integer between 0 and 127. ### `.sendPitchBend(...)` {#sendPitchBend} **Since**: 3.0.0
Sends a MIDI **pitch bend** message to the specified channel(s) at the scheduled time. The resulting bend is relative to the pitch bend range that has been defined. The range can be set with [`sendPitchBendRange()`](#sendPitchBendRange). So, for example, if the pitch bend range has been set to 12 semitones, using a bend value of `-1` will bend the note 1 octave below its nominal value. **Parameters** > Signature: `sendPitchBend(value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
Array.<number>
||The intensity of the bend (between `-1.0` and `1.0`). A value of `0` means no bend. If an invalid value is specified, the nearest valid value will be used instead. If the `rawValue` option is set to `true`, the intensity of the bend can be defined by either using a single integer between `0` and `127` (MSB) or an array of two integers between `0` and `127` representing, respectively, the MSB (most significant byte) and the LSB (least significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value lower than `64` bends downwards while a value higher than `64` bends upwards. The LSB is expressed in cents (1/100 of a semitone). An LSB of `64` also means no bend.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered as a float between `-1.0` and `1.0` (default) or as raw integer between `0` and 127` (or an array of 2 integers if using both MSB and LSB).| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendPitchBendRange(...)` {#sendPitchBendRange} **Since**: 3.0.0
Sends a **pitch bend range** message to the specified channel(s) at the scheduled time so that they adjust the range used by their pitch bend lever. The range is specified by using the `semitones` and `cents` parameters. For example, setting the `semitones` parameter to `12` means that the pitch bend range will be 12 semitones above and below the nominal pitch. **Parameters** > Signature: `sendPitchBendRange([semitones], [cents], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`semitones`**] | number
|0|The desired adjustment value in semitones (between `0` and `127`). While nothing imposes that in the specification, it is very common for manufacturers to limit the range to 2 octaves (-12 semitones to 12 semitones).| |[**`cents`**] | number
|0|The desired adjustment value in cents (integer between `0` and `127`).| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The msb value must be between 0 and 127. * `RangeError` : The lsb value must be between 0 and 127. ### `.sendPolyphonicMode(...)` {#sendPolyphonicMode} **Since**: 3.0.0
Sets the polyphonic mode. In `poly` mode (usually the default), multiple notes can be played and heard at the same time. In `mono` mode, only one note will be heard at once even if multiple notes are being played. **Parameters** > Signature: `sendPolyphonicMode(mode, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`mode`** | string
||The mode to use: `mono` or `poly`.| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendProgramChange(...)` {#sendProgramChange} **Since**: 3.0.0
Sends a MIDI **program change** message to the specified channel(s) at the scheduled time. **Parameters** > Signature: `sendProgramChange([program], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`program`**] | number
|0|The MIDI patch (program) number (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `TypeError` : Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater than 0xFF. ### `.sendReset(...)` {#sendReset} Sends a **reset** real-time message. This tells the device connected to this output that it should reset itself to a default state. **Parameters** > Signature: `sendReset([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendResetAllControllers(...)` {#sendResetAllControllers} Sends a **reset all controllers** channel mode message. This resets all controllers, such as the pitch bend, to their default value. **Parameters** > Signature: `sendResetAllControllers([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
### `.sendRpnDecrement(...)` {#sendRpnDecrement} Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter names that can be used with this method: * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * Tuning Program (0x00, 0x03): `"tuningprogram"` * Tuning Bank (0x00, 0x04): `"tuningbank"` * Modulation Range (0x00, 0x05): `"modulationrange"` * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * Elevation Angle (0x3D, 0x01): `"elevationangle"` * Gain (0x3D, 0x02): `"gain"` * Distance Ratio (0x3D, 0x03): `"distanceratio"` * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * Roll Angle (0x3D, 0x08): `"rollangle"` **Parameters** > Signature: `sendRpnDecrement(parameter, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | String
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (0x65, 0x64) that identify the registered parameter.| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * TypeError The specified parameter is not available. ### `.sendRpnIncrement(...)` {#sendRpnIncrement} Increments the specified MIDI registered parameter by 1. Here is the full list of parameter names that can be used with this method: * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * Tuning Program (0x00, 0x03): `"tuningprogram"` * Tuning Bank (0x00, 0x04): `"tuningbank"` * Modulation Range (0x00, 0x05): `"modulationrange"` * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * Elevation Angle (0x3D, 0x01): `"elevationangle"` * Gain (0x3D, 0x02): `"gain"` * Distance Ratio (0x3D, 0x03): `"distanceratio"` * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * Roll Angle (0x3D, 0x08): `"rollangle"` **Parameters** > Signature: `sendRpnIncrement(parameter, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | String
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (0x65, 0x64) that identify the registered parameter.| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendRpnValue(...)` {#sendRpnValue} Sets the specified MIDI registered parameter to the desired value. The value is defined with up to two bytes of data (msb, lsb) that each can go from `0` to `127`. MIDI [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) extend the original list of control change messages. The MIDI 1.0 specification lists only a limited number of them: | Numbers | Function | |--------------|--------------------------| | (0x00, 0x00) | `pitchbendrange` | | (0x00, 0x01) | `channelfinetuning` | | (0x00, 0x02) | `channelcoarsetuning` | | (0x00, 0x03) | `tuningprogram` | | (0x00, 0x04) | `tuningbank` | | (0x00, 0x05) | `modulationrange` | | (0x3D, 0x00) | `azimuthangle` | | (0x3D, 0x01) | `elevationangle` | | (0x3D, 0x02) | `gain` | | (0x3D, 0x03) | `distanceratio` | | (0x3D, 0x04) | `maximumdistance` | | (0x3D, 0x05) | `maximumdistancegain` | | (0x3D, 0x06) | `referencedistanceratio` | | (0x3D, 0x07) | `panspreadangle` | | (0x3D, 0x08) | `rollangle` | Note that the `tuningprogram` and `tuningbank` parameters are part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendRpnValue(parameter, [data], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | string
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the registered parameter.| |[**`data`**] | number
Array.<number>
|[]|A single integer or an array of integers with a maximum length of 2 specifying the desired data.| |[**`options`**] | object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendSongPosition(...)` {#sendSongPosition} **Since**: 3.0.0
Sends a **song position** MIDI message. The value is expressed in MIDI beats (between `0` and `16383`) which are 16th note. Position `0` is always the start of the song. **Parameters** > Signature: `sendSongPosition([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
|0|The MIDI beat to cue to (integer between `0` and `16383`).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendSongSelect(...)` {#sendSongSelect} **Since**: 3.0.0
Sends a **song select** MIDI message. **Parameters** > Signature: `sendSongSelect([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
|0|The number of the song to select (integer between `0` and `127`).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * The song number must be between 0 and 127. ### `.sendStart(...)` {#sendStart} Sends a **start** real-time message. A MIDI Start message starts the playback of the current song at beat 0. To start playback elsewhere in the song, use the [`sendContinue()`](#sendContinue) method. **Parameters** > Signature: `sendStart([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendStop(...)` {#sendStop} Sends a **stop** real-time message. This tells the device connected to this output to stop playback immediately (or at the scheduled time, if specified). **Parameters** > Signature: `sendStop([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendSysex(...)` {#sendSysex} Sends a MIDI [**system exclusive**](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages) (*sysex*) message. There are two categories of system exclusive messages: manufacturer-specific messages and universal messages. Universal messages are further divided into three subtypes: * Universal non-commercial (for research and testing): `0x7D` * Universal non-realtime: `0x7E` * Universal realtime: `0x7F` The method's first parameter (`identification`) identifies the type of message. If the value of `identification` is `0x7D` (125), `0x7E` (126) or `0x7F` (127), the message will be identified as a **universal non-commercial**, **universal non-realtime** or **universal realtime** message (respectively). If the `identification` value is an array or an integer between 0 and 124, it will be used to identify the manufacturer targeted by the message. The *MIDI Manufacturers Association* maintains a full list of [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers). The `data` parameter should only contain the data of the message. When sending out the actual MIDI message, WEBMIDI.js will automatically prepend the data with the **sysex byte** (`0xF0`) and the identification byte(s). It will also automatically terminate the message with the **sysex end byte** (`0xF7`). To use the `sendSysex()` method, system exclusive message support must have been enabled. To do so, you must set the `sysex` option to `true` when calling [`WebMidi.enable()`](WebMidi#enable): ```js WebMidi.enable({sysex: true}) .then(() => console.log("System exclusive messages are enabled"); ``` ##### Examples of manufacturer-specific system exclusive messages If you want to send a sysex message to a Korg device connected to the first output, you would use the following code: ```js WebMidi.outputs[0].sendSysex(0x42, [0x1, 0x2, 0x3, 0x4, 0x5]); ``` In this case `0x42` is the ID of the manufacturer (Korg) and `[0x1, 0x2, 0x3, 0x4, 0x5]` is the data being sent. The parameters can be specified using any number notation (decimal, hex, binary, etc.). Therefore, the code above is equivalent to this code: ```js WebMidi.outputs[0].sendSysex(66, [1, 2, 3, 4, 5]); ``` Some manufacturers are identified using 3 bytes. In this case, you would use a 3-position array as the first parameter. For example, to send the same sysex message to a *Native Instruments* device: ```js WebMidi.outputs[0].sendSysex([0x00, 0x21, 0x09], [0x1, 0x2, 0x3, 0x4, 0x5]); ``` There is no limit for the length of the data array. However, it is generally suggested to keep system exclusive messages to 64Kb or less. ##### Example of universal system exclusive message If you want to send a universal sysex message, simply assign the correct identification number in the first parameter. Number `0x7D` (125) is for non-commercial, `0x7E` (126) is for non-realtime and `0x7F` (127) is for realtime. So, for example, if you wanted to send an identity request non-realtime message (`0x7E`), you could use the following: ```js WebMidi.outputs[0].sendSysex(0x7E, [0x7F, 0x06, 0x01]); ``` For more details on the format of universal messages, consult the list of [universal sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages). **Parameters** > Signature: `sendSysex(identification, [data], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`identification`** | number
Array.<number>
||An unsigned integer or an array of three unsigned integers between `0` and `127` that either identify the manufacturer or sets the message to be a **universal non-commercial message** (`0x7D`), a **universal non-realtime message** (`0x7E`) or a **universal realtime message** (`0x7F`). The *MIDI Manufacturers Association* maintains a full list of [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers).| |[**`data`**] | Array.<number>
Uint8Array
||A `Uint8Array` or an array of unsigned integers between `0` and `127`. This is the data you wish to transfer.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `DOMException` : Failed to execute 'send' on 'MIDIOutput': System exclusive message is not allowed. * `TypeError` : Failed to execute 'send' on 'MIDIOutput': The value at index x is greater than 0xFF. ### `.sendTimecodeQuarterFrame(...)` {#sendTimecodeQuarterFrame} Sends a MIDI **timecode quarter frame** message. Please note that no processing is being done on the data. It is up to the developer to format the data according to the [MIDI Timecode](https://en.wikipedia.org/wiki/MIDI_timecode) format. **Parameters** > Signature: `sendTimecodeQuarterFrame(value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||The quarter frame message content (integer between 0 and 127).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendTuneRequest(...)` {#sendTuneRequest} **Since**: 3.0.0
Sends a MIDI **tune request** real-time message. **Parameters** > Signature: `sendTuneRequest([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.sendTuningBank(...)` {#sendTuningBank} **Since**: 3.0.0
Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendTuningBank([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
|0|The desired tuning bank (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The bank value must be between 0 and 127. ### `.sendTuningProgram(...)` {#sendTuningProgram} **Since**: 3.0.0
Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendTuningProgram(value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||The desired tuning program (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. **Throws**: * `RangeError` : The program value must be between 0 and 127. ### `.stopNote(...)` {#stopNote} Sends a **note off** message for the specified MIDI note number on the specified channel(s). The first parameter is the note to stop. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The execution of the **note off** command can be delayed by using the `time` property of the `options` parameter. **Parameters** > Signature: `stopNote(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) to stop. The notes can be specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`options`**] | Object
|{}|| |[**`options.channels`**] | number
Array.<number>
|[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]|The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no channel is specified, all channels will be used.| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `64`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
*** ## Events ### `closed` {#event-closed} Event emitted when the [Output](Output) has been closed by calling the [close()](Output#close) method. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`"closed"`| |**`target`** |Output|The object to which the listener was originally added (`Output`).| |**`port`** |Output|The port that was closed| ### `disconnected` {#event-disconnected} Event emitted when the [Output](Output) becomes unavailable. This event is typically fired when the MIDI device is unplugged. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp0 when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`"disconnected"`| |**`target`** |Output|The object to which the listener was originally added (`Output`).| |**`port`** |object|Object with properties describing the [Output](Output) that was disconnected. This is not the actual `Output` as it is no longer available.| ### `opened` {#event-opened} Event emitted when the [Output](Output) has been opened by calling the [open()](Output#open) method. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`"opened"`| |**`target`** |Output|The object to which the listener was originally added (`Output`).| |**`port`** |Output|The port that was opened| ================================================ FILE: website/api/classes/OutputChannel.md ================================================ # OutputChannel The `OutputChannel` class represents a single output MIDI channel. `OutputChannel` objects are provided by an [`Output`](Output) port which, itself, is made available by a device. The `OutputChannel` object is derived from the host's MIDI subsystem and should not be instantiated directly. All 16 `OutputChannel` objects can be found inside the parent output's [`channels`](Output#channels) property. **Since**: 3.0.0 **Extends**: [`EventEmitter`](EventEmitter) ### `Constructor` Creates an `OutputChannel` object. **Parameters** > `new OutputChannel(output, number)`
| Parameter | Type | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`output`** | Output
||The [`Output`](Output) this channel belongs to.| |**`number`** | number
||The MIDI channel number (`1` - `16`).|
*** ## Properties ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. ### `.number` {#number} **Since**: 3.0
**Type**: number
This channel's MIDI number (`1` - `16`). ### `.octaveOffset` {#octaveOffset} **Since**: 3.0
**Type**: number
An integer to offset the reported octave of outgoing note-specific messages (`noteon`, `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th octave (C4). Note that this value is combined with the global offset value defined in [`WebMidi.octaveOffset`](WebMidi#octaveOffset) and with the parent value defined in [`Output.octaveOffset`](Output#octaveOffset). ### `.output` {#output} **Since**: 3.0
**Type**: Output
The parent [`Output`](Output) this channel belongs to. *** ## Methods ### `.addListener(...)` {#addListener} Adds a listener for the specified event. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to.| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs.| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The value of `this` in the callback function.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time listener for the specified event. The listener will be executed once and then destroyed. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addOneTimeListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The context to invoke the callback function in.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.hasListener(...)` {#hasListener} Returns `true` if the specified event has at least one registered listener. If no event is specified, the method returns `true` if any event has at least one listener registered (this includes global listeners registered to [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). Note: to specifically check for global listeners added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT), use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `hasListener([event], [callback])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
Symbol
|(any event)|The event to check| |[**`callback`**] | function
Listener
|(any callback)|The actual function that was added to the event or the [Listener](Listener) object returned by `addListener()`.|
**Return Value** > Returns: `boolean`
### `.playNote(...)` {#playNote} Plays a note or an array of notes on the channel. The first parameter is the note to play. It can be a single value or an array of the following valid values: - A [`Note`](Note) object - A MIDI note number (integer between `0` and `127`) - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) The `playNote()` method sends a **note on** MIDI message for all specified notes. If a `duration` is set in the `options` parameter or in the [`Note`](Note) object's [`duration`](Note#duration) property, it will also schedule a **note off** message to end the note after said duration. If no `duration` is set, the note will simply play until a matching **note off** message is sent with [`stopNote()`](#OutputChannel+stopNote) or [`sendNoteOff()`](#OutputChannel+sendNoteOff). The execution of the **note on** command can be delayed by using the `time` property of the `options` parameter. When using [`Note`](Note) objects, the durations and velocities defined in the [`Note`](Note) objects have precedence over the ones specified via the method's `options` parameter. **Note**: per the MIDI standard, a **note on** message with an attack velocity of `0` is functionally equivalent to a **note off** message. **Parameters** > Signature: `playNote(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
string
Note
Array.<number>
Array.<string>
Array.<Note>
||The note(s) to play. The notes can be specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`), a [`Note`](Note) object or an array of the previous types. When using a note identifier, the octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`options`**] | object
|{}|| |[**`options.duration`**] | number
||A positive decimal number larger than `0` representing the number of milliseconds to wait before sending a **note off** message. If invalid or left undefined, only a **note on** message will be sent.| |[**`options.attack`**] | number
|0.5|The velocity at which to play the note (between `0` and `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawAttack`**] | number
|64|The attack velocity at which to play the note (between `0` and `127`). This has priority over the `attack` property. An invalid velocity value will silently trigger the default of 64.| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid velocity value will silently trigger the default of `0.5`. This is only used with the **note off** event triggered when `options.duration` is set.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). This has priority over the `release` property. An invalid velocity value will silently trigger the default of 64. This is only used with the **note off** event triggered when `options.duration` is set.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.removeListener(...)` {#removeListener} Removes all the listeners that were added to the object upon which the method is called and that match the specified criterias. If no parameters are passed, all listeners added to this object will be removed. If only the `event` parameter is passed, all listeners for that event will be removed from that object. You can remove global listeners by using [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. To use more granular options, you must at least define the `event`. Then, you can specify the callback to match or one or more of the additional options. **Parameters** > Signature: `removeListener([event], [callback], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
||The event name.| |[**`callback`**] | EventEmitter~callback
||Only remove the listeners that match this exact callback function.| |[**`options`**] | Object
||| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.send(...)` {#send} Sends a MIDI message on the MIDI output port. If no time is specified, the message will be sent immediately. The message should be an array of 8-bit unsigned integers (`0` - `225`), a [`Uint8Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) object or a [`Message`](Message) object. It is usually not necessary to use this method directly as you can use one of the simpler helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote), [`sendControlChange()`](#sendControlChange), etc. Details on the format of MIDI messages are available in the summary of [MIDI messages](https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message) from the MIDI Manufacturers Association. **Parameters** > Signature: `send(message, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`message`** | Array.<number>
Uint8Array
Message
||A `Message` object, an array of 8-bit unsigned integers or a `Uint8Array` object (not available in Node.js) containing the message bytes.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The first byte (status) must be an integer between 128 and 255. * `RangeError` : Data bytes must be integers between 0 and 255. ### `.sendAllNotesOff(...)` {#sendAllNotesOff} Sends an **all notes off** channel mode message. This will make all currently playing notes fade out just as if their key had been released. This is different from the [`sendAllSoundOff()`](#sendAllSoundOff) method which mutes all sounds immediately. **Parameters** > Signature: `sendAllNotesOff([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendAllSoundOff(...)` {#sendAllSoundOff} Sends an **all sound off** channel mode message. This will silence all sounds playing on that channel but will not prevent new sounds from being triggered. **Parameters** > Signature: `sendAllSoundOff([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendChannelAftertouch(...)` {#sendChannelAftertouch} Sends a MIDI **channel aftertouch** message. For key-specific aftertouch, you should instead use [`sendKeyAftertouch()`](#sendKeyAftertouch). **Parameters** > Signature: `sendChannelAftertouch([pressure], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`pressure`**] | number
||The pressure level (between `0` and `1`). If the `rawValue` option is set to `true`, the pressure can be defined by using an integer between `0` and `127`.| |[**`options`**] | object
|{}|| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * RangeError Invalid channel aftertouch value. ### `.sendChannelMode(...)` {#sendChannelMode} Sends a MIDI **channel mode** message. The channel mode message to send can be specified numerically or by using one of the following common names: | Type |Number| Shortcut Method | | ---------------------|------|-------------------------------------------------------------- | | `allsoundoff` | 120 | [`sendAllSoundOff()`](#sendAllSoundOff) | | `resetallcontrollers`| 121 | [`sendResetAllControllers()`](#sendResetAllControllers) | | `localcontrol` | 122 | [`sendLocalControl()`](#sendLocalControl) | | `allnotesoff` | 123 | [`sendAllNotesOff()`](#sendAllNotesOff) | | `omnimodeoff` | 124 | [`sendOmniMode(false)`](#sendOmniMode) | | `omnimodeon` | 125 | [`sendOmniMode(true)`](#sendOmniMode) | | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`](#sendPolyphonicMode) | | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`](#sendPolyphonicMode) | **Note**: as you can see above, to make it easier, all channel mode messages also have a matching helper method. It should be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` may require a value that's not zero. For that reason, the `value` parameter is optional and defaults to 0. **Parameters** > Signature: `sendChannelMode(command, [value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`command`** | number
string
||The numerical identifier of the channel mode message (integer between `120` and `127`) or its name as a string.| |[**`value`**] | number
|0|The value to send (integer between `0` - `127`).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendControlChange(...)` {#sendControlChange} **Since**: 3.0.0
Sends a MIDI **control change** message to the channel at the scheduled time. The control change message to send can be specified numerically (`0` to `127`) or by using one of the following common names: | Number | Name | |--------|-------------------------------| | 0 |`bankselectcoarse` | | 1 |`modulationwheelcoarse` | | 2 |`breathcontrollercoarse` | | 4 |`footcontrollercoarse` | | 5 |`portamentotimecoarse` | | 6 |`dataentrycoarse` | | 7 |`volumecoarse` | | 8 |`balancecoarse` | | 10 |`pancoarse` | | 11 |`expressioncoarse` | | 12 |`effectcontrol1coarse` | | 13 |`effectcontrol2coarse` | | 18 |`generalpurposeslider3` | | 19 |`generalpurposeslider4` | | 32 |`bankselectfine` | | 33 |`modulationwheelfine` | | 34 |`breathcontrollerfine` | | 36 |`footcontrollerfine` | | 37 |`portamentotimefine` | | 38 |`dataentryfine` | | 39 |`volumefine` | | 40 |`balancefine` | | 42 |`panfine` | | 43 |`expressionfine` | | 44 |`effectcontrol1fine` | | 45 |`effectcontrol2fine` | | 64 |`holdpedal` | | 65 |`portamento` | | 66 |`sustenutopedal` | | 67 |`softpedal` | | 68 |`legatopedal` | | 69 |`hold2pedal` | | 70 |`soundvariation` | | 71 |`resonance` | | 72 |`soundreleasetime` | | 73 |`soundattacktime` | | 74 |`brightness` | | 75 |`soundcontrol6` | | 76 |`soundcontrol7` | | 77 |`soundcontrol8` | | 78 |`soundcontrol9` | | 79 |`soundcontrol10` | | 80 |`generalpurposebutton1` | | 81 |`generalpurposebutton2` | | 82 |`generalpurposebutton3` | | 83 |`generalpurposebutton4` | | 91 |`reverblevel` | | 92 |`tremololevel` | | 93 |`choruslevel` | | 94 |`celestelevel` | | 95 |`phaserlevel` | | 96 |`dataincrement` | | 97 |`datadecrement` | | 98 |`nonregisteredparametercoarse` | | 99 |`nonregisteredparameterfine` | | 100 |`registeredparametercoarse` | | 101 |`registeredparameterfine` | | 120 |`allsoundoff` | | 121 |`resetallcontrollers` | | 122 |`localcontrol` | | 123 |`allnotesoff` | | 124 |`omnimodeoff` | | 125 |`omnimodeon` | | 126 |`monomodeon` | | 127 |`polymodeon` | As you can see above, not all control change message have a matching name. This does not mean you cannot use the others. It simply means you will need to use their number (`0` to `127`) instead of their name. While you can still use them, numbers `120` to `127` are usually reserved for *channel mode* messages. See [`sendChannelMode()`](#OutputChannel+sendChannelMode) method for more info. To view a detailed list of all available **control change** messages, please consult "Table 3 - Control Change Messages" from the [MIDI Messages]( https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2) specification. **Note**: messages #0-31 (MSB) are paired with messages #32-63 (LSB). For example, message #1 (`modulationwheelcoarse`) can be accompanied by a second control change message for `modulationwheelfine` to achieve a greater level of precision. if you want to specify both MSB and LSB for messages between `0` and `31`, you can do so by passing a 2-value array as the second parameter. **Parameters** > Signature: `sendControlChange(controller, value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`controller`** | number
string
||The MIDI controller name or number (`0` - `127`).| |**`value`** | number
Array.<number>
||The value to send (0-127). You can also use a two-position array for controllers 0 to 31. In this scenario, the first value will be sent as usual and the second value will be sent to the matching LSB controller (which is obtained by adding 32 to the first controller)| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : Controller numbers must be between 0 and 127. * `RangeError` : Invalid controller name. * `TypeError` : The value array must have a length of 2. ### `.sendKeyAftertouch(...)` {#sendKeyAftertouch} Sends a MIDI **key aftertouch** message at the scheduled time. This is a key-specific aftertouch. For a channel-wide aftertouch message, use [`sendChannelAftertouch()`](#sendChannelAftertouch). **Parameters** > Signature: `sendKeyAftertouch(target, [pressure], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`target`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) for which you are sending an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`). When using a note identifier, the octave value will be offset by the local [`octaveOffset`](#octaveOffset) and by [`Output.octaveOffset`](Output#octaveOffset) and [`WebMidi.octaveOffset`](WebMidi#octaveOffset) (if those values are not `0`). When using a key number, `octaveOffset` values are ignored.| |[**`pressure`**] | number
|0.5|The pressure level (between `0` and `1`). An invalid pressure value will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the pressure is defined by using an integer between `0` and `127`.| |[**`options`**] | object
|{}|| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * RangeError Invalid key aftertouch value. ### `.sendLocalControl(...)` {#sendLocalControl} Turns local control on or off. Local control is usually enabled by default. If you disable it, the instrument will no longer trigger its own sounds. It will only send the MIDI messages to its out port. **Parameters** > Signature: `sendLocalControl([state], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`state`**] | boolean
|false|Whether to activate local control (`true`) or disable it (`false`).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendMasterTuning(...)` {#sendMasterTuning} Sends a **master tuning** message. The value is decimal and must be larger than -65 semitones and smaller than 64 semitones. Because of the way the MIDI specification works, the decimal portion of the value will be encoded with a resolution of 14bit. The integer portion must be between -64 and 63 inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and a **Master Fine Tuning** RPN messages. **Parameters** > Signature: `sendMasterTuning([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
|0.0|The desired decimal adjustment value in semitones (-65 < x < 64)| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The value must be a decimal number between larger than -65 and smaller than 64. ### `.sendModulationRange(...)` {#sendModulationRange} Sends a **modulation depth range** message to adjust the depth of the modulation wheel's range. The range can be specified with the `semitones` parameter, the `cents` parameter or by specifying both parameters at the same time. **Parameters** > Signature: `sendModulationRange(semitones, [cents], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`semitones`** | number
||The desired adjustment value in semitones (integer between 0 and 127).| |[**`cents`**] | number
|0|The desired adjustment value in cents (integer between 0 and 127).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendNoteOff(...)` {#sendNoteOff} Sends a **note off** message for the specified notes on the channel. The first parameter is the note. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The execution of the **note off** command can be delayed by using the `time` property of the `options` parameter. When using [`Note`](Note) objects, the release velocity defined in the [`Note`](Note) objects has precedence over the one specified via the method's `options` parameter. **Parameters** > Signature: `sendNoteOff(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
string
Note
Array.<number>
Array.<string>
Array.<Note>
||The note(s) to stop. The notes can be specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a [`Note`](Note) object or an array of the previous types. When using a note name, octave range must be between -1 and 9. The lowest note is C-1 (MIDI note number 0) and the highest note is G9 (MIDI note number 127).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `64`.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendNoteOn(...)` {#sendNoteOn} Sends a **note on** message for the specified note(s) on the channel. The first parameter is the note. It can be a single value or an array of the following valid values: - A [`Note`](Note) object - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) When passing a [`Note`](Note)object or a note name, the `octaveOffset` will be applied. This is not the case when using a note number. In this case, we assume you know exactly which MIDI note number should be sent out. The execution of the **note on** command can be delayed by using the `time` property of the `options` parameter. When using [`Note`](Note) objects, the attack velocity defined in the [`Note`](Note) objects has precedence over the one specified via the method's `options` parameter. Also, the `duration` is ignored. If you want to also send a **note off** message, use the [`playNote()`](#playNote) method instead. **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is functionally equivalent to a **note off** message. **Parameters** > Signature: `sendNoteOn(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
string
Note
Array.<number>
Array.<string>
Array.<Note>
||The note(s) to play. The notes can be specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a [`Note`](Note) object or an array of the previous types.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.| |[**`options.attack`**] | number
|0.5|The velocity at which to play the note (between `0` and `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawAttack`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid velocity value will silently trigger the default of `64`.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendNrpnValue(...)` {#sendNrpnValue} Sets a non-registered parameter (NRPN) to the specified value. The NRPN is selected by passing in a two-position array specifying the values of the two control bytes. The value is specified by passing in a single integer (most cases) or an array of two integers. NRPNs are not standardized in any way. Each manufacturer is free to implement them any way they see fit. For example, according to the Roland GS specification, you can control the **vibrato rate** using NRPN (1, 8). Therefore, to set the **vibrato rate** value to **123** you would use: ```js WebMidi.outputs[0].channels[0].sendNrpnValue([1, 8], 123); ``` In some rarer cases, you need to send two values with your NRPN messages. In such cases, you would use a 2-position array. For example, for its **ClockBPM** parameter (2, 63), Novation uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the value to send was 10, you could use: ```js WebMidi.outputs[0].channels[0].sendNrpnValue([2, 63], [0, 10]); ``` For further implementation details, refer to the manufacturer's documentation. **Parameters** > Signature: `sendNrpnValue(nrpn, [data], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`nrpn`** | Array.<number>
||A two-position array specifying the two control bytes (0x63, 0x62) that identify the non-registered parameter.| |[**`data`**] | number
Array.<number>
|[]|An integer or an array of integers with a length of 1 or 2 specifying the desired data.| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The control value must be between 0 and 127. * `RangeError` : The msb value must be between 0 and 127 ### `.sendOmniMode(...)` {#sendOmniMode} Sets OMNI mode to `"on"` or `"off"`. MIDI's OMNI mode causes the instrument to respond to messages from all channels. It should be noted that support for OMNI mode is not as common as it used to be. **Parameters** > Signature: `sendOmniMode([state], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`state`**] | boolean
|true|Whether to activate OMNI mode (`true`) or not (`false`).| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `TypeError` : Invalid channel mode message name. * `RangeError` : Channel mode controller numbers must be between 120 and 127. * `RangeError` : Value must be an integer between 0 and 127. ### `.sendPitchBend(...)` {#sendPitchBend} Sends a MIDI **pitch bend** message at the scheduled time. The resulting bend is relative to the pitch bend range that has been defined. The range can be set with [`sendPitchBendRange()`](#sendPitchBendRange). So, for example, if the pitch bend range has been set to 12 semitones, using a bend value of -1 will bend the note 1 octave below its nominal value. **Parameters** > Signature: `sendPitchBend([value], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`value`**] | number
Array.<number>
||The intensity of the bend (between -1.0 and 1.0). A value of zero means no bend. If the `rawValue` option is set to `true`, the intensity of the bend can be defined by either using a single integer between 0 and 127 (MSB) or an array of two integers between 0 and 127 representing, respectively, the MSB (most significant byte) and the LSB (least significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value lower than `64` bends downwards while a value higher than `64` bends upwards. The LSB is expressed in cents (1/100 of a semitone). An LSB of `64` also means no bend.| |[**`options`**] | Object
|{}|| |[**`options.rawValue`**] | boolean
|false|A boolean indicating whether the value should be considered as a float between -1.0 and 1.0 (default) or as raw integer between 0 and 127 (or an array of 2 integers if using both MSB and LSB).| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendPitchBendRange(...)` {#sendPitchBendRange} Sends a **pitch bend range** message at the scheduled time to adjust the range used by the pitch bend lever. The range is specified by using the `semitones` and `cents` parameters. For example, setting the `semitones` parameter to `12` means that the pitch bend range will be 12 semitones above and below the nominal pitch. **Parameters** > Signature: `sendPitchBendRange(semitones, [cents], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`semitones`** | number
||The desired adjustment value in semitones (between 0 and 127). While nothing imposes that in the specification, it is very common for manufacturers to limit the range to 2 octaves (-12 semitones to 12 semitones).| |[**`cents`**] | number
|0|The desired adjustment value in cents (integer between 0-127).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The semitones value must be an integer between 0 and 127. * `RangeError` : The cents value must be an integer between 0 and 127. ### `.sendPolyphonicMode(...)` {#sendPolyphonicMode} Sets the polyphonic mode. In `"poly"` mode (usually the default), multiple notes can be played and heard at the same time. In `"mono"` mode, only one note will be heard at once even if multiple notes are being played. **Parameters** > Signature: `sendPolyphonicMode([mode], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`mode`**] | string
|poly|The mode to use: `"mono"` or `"poly"`.| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendProgramChange(...)` {#sendProgramChange} Sends a MIDI **program change** message at the scheduled time. **Parameters** > Signature: `sendProgramChange([program], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`program`**] | number
|1|The MIDI patch (program) number (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `TypeError` : Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater than 0xFF. ### `.sendResetAllControllers(...)` {#sendResetAllControllers} Sends a **reset all controllers** channel mode message. This resets all controllers, such as the pitch bend, to their default value. **Parameters** > Signature: `sendResetAllControllers([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendRpnDecrement(...)` {#sendRpnDecrement} Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter names that can be used with this function: * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * Tuning Program (0x00, 0x03): `"tuningprogram"` * Tuning Bank (0x00, 0x04): `"tuningbank"` * Modulation Range (0x00, 0x05): `"modulationrange"` * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * Elevation Angle (0x3D, 0x01): `"elevationangle"` * Gain (0x3D, 0x02): `"gain"` * Distance Ratio (0x3D, 0x03): `"distanceratio"` * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * Roll Angle (0x3D, 0x08): `"rollangle"` **Parameters** > Signature: `sendRpnDecrement(parameter, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | String
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (0x65, 0x64) that identify the registered parameter.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * TypeError The specified registered parameter is invalid. ### `.sendRpnIncrement(...)` {#sendRpnIncrement} Increments the specified MIDI registered parameter by 1. Here is the full list of parameter names that can be used with this function: * Pitchbend Range (0x00, 0x00): `"pitchbendrange"` * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"` * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"` * Tuning Program (0x00, 0x03): `"tuningprogram"` * Tuning Bank (0x00, 0x04): `"tuningbank"` * Modulation Range (0x00, 0x05): `"modulationrange"` * Azimuth Angle (0x3D, 0x00): `"azimuthangle"` * Elevation Angle (0x3D, 0x01): `"elevationangle"` * Gain (0x3D, 0x02): `"gain"` * Distance Ratio (0x3D, 0x03): `"distanceratio"` * Maximum Distance (0x3D, 0x04): `"maximumdistance"` * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"` * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"` * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"` * Roll Angle (0x3D, 0x08): `"rollangle"` **Parameters** > Signature: `sendRpnIncrement(parameter, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`parameter`** | String
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (0x65, 0x64) that identify the registered parameter.| |[**`options`**] | object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * TypeError The specified registered parameter is invalid. ### `.sendRpnValue(...)` {#sendRpnValue} Sets the specified MIDI registered parameter to the desired value. The value is defined with up to two bytes of data (msb, lsb) that each can go from 0 to 127. MIDI [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2) extend the original list of control change messages. The MIDI 1.0 specification lists only a limited number of them: | Numbers | Function | |--------------|--------------------------| | (0x00, 0x00) | `pitchbendrange` | | (0x00, 0x01) | `channelfinetuning` | | (0x00, 0x02) | `channelcoarsetuning` | | (0x00, 0x03) | `tuningprogram` | | (0x00, 0x04) | `tuningbank` | | (0x00, 0x05) | `modulationrange` | | (0x3D, 0x00) | `azimuthangle` | | (0x3D, 0x01) | `elevationangle` | | (0x3D, 0x02) | `gain` | | (0x3D, 0x03) | `distanceratio` | | (0x3D, 0x04) | `maximumdistance` | | (0x3D, 0x05) | `maximumdistancegain` | | (0x3D, 0x06) | `referencedistanceratio` | | (0x3D, 0x07) | `panspreadangle` | | (0x3D, 0x08) | `rollangle` | Note that the **Tuning Program** and **Tuning Bank** parameters are part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendRpnValue(rpn, [data], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`rpn`** | string
Array.<number>
||A string identifying the parameter's name (see above) or a two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the registered parameter.| |[**`data`**] | number
Array.<number>
|[]|An single integer or an array of integers with a maximum length of 2 specifying the desired data.| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. ### `.sendTuningBank(...)` {#sendTuningBank} Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendTuningBank(value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||The desired tuning bank (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The bank value must be between 0 and 127. ### `.sendTuningProgram(...)` {#sendTuningProgram} Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the *MIDI Tuning Standard*, which is not widely implemented. **Parameters** > Signature: `sendTuningProgram(value, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||The desired tuning program (integer between `0` and `127`).| |[**`options`**] | Object
|{}|| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `OutputChannel`
Returns the `OutputChannel` object so methods can be chained. **Throws**: * `RangeError` : The program value must be between 0 and 127. ### `.stopNote(...)` {#stopNote} Sends a **note off** message for the specified MIDI note number. The first parameter is the note to stop. It can be a single value or an array of the following valid values: - A MIDI note number (integer between `0` and `127`) - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`) - A [`Note`](Note) object The execution of the **note off** command can be delayed by using the `time` property of the `options` parameter. **Parameters** > Signature: `stopNote(note, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`note`** | number
Note
string
Array.<number>
Array.<Note>
Array.<string>
||The note(s) to stop. The notes can be specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number `127`).| |[**`options`**] | Object
|{}|| |[**`options.release`**] | number
|0.5|The velocity at which to release the note (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `0.5`.| |[**`options.rawRelease`**] | number
|64|The velocity at which to release the note (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have priority. An invalid velocity value will silently trigger the default of `64`.| |[**`options.time`**] | number
string
|(now)|If `time` is a string prefixed with `"+"` and followed by a number, the message will be delayed by that many milliseconds. If the value is a positive number ([`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)), the operation will be scheduled for that time. The current time can be retrieved with [`WebMidi.time`](WebMidi#time). If `options.time` is omitted, or in the past, the operation will be carried out as soon as possible.|
**Return Value** > Returns: `Output`
Returns the `Output` object so methods can be chained. ### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
================================================ FILE: website/api/classes/Utilities.md ================================================ # Utilities The `Utilities` class contains general-purpose utility methods. All methods are static and should be called using the class name. For example: `Utilities.getNoteDetails("C4")`. **Since**: 3.0.0 *** ## Properties ### `.isBrowser` {#isBrowser} **Type**: boolean
Indicates whether the execution environment is a browser (`true`) or not (`false`) ### `.isNode` {#isNode} **Type**: boolean
Indicates whether the execution environment is Node.js (`true`) or not (`false`) *** ## Methods ### `.buildNote(...)` {#buildNote} **Since**: version 3.0.0
Converts the `input` parameter to a valid [`Note`](Note) object. The input usually is an unsigned integer (0-127) or a note identifier (`"C4"`, `"G#5"`, etc.). If the input is a [`Note`](Note) object, it will be returned as is. If the input is a note number or identifier, it is possible to specify options by providing the `options` parameter. **Parameters** > Signature: `buildNote([input], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`input`**] | number
string
Note
||| |[**`options`**] | object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the note should be explicitly stopped.| |[**`options.attack`**] | number
|0.5|The note's attack velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.release`**] | number
|0.5|The note's release velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.| |[**`options.rawAttack`**] | number
|64|The note's attack velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.rawRelease`**] | number
|64|The note's release velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.| |[**`options.octaveOffset`**] | number
|0|An integer to offset the octave by. **This is only used when the input value is a note identifier.**|
**Return Value** > Returns: `Note`
**Attributes**: static **Throws**: * TypeError The input could not be parsed to a note ### `.buildNoteArray(...)` {#buildNoteArray} **Since**: 3.0.0
Converts an input value, which can be an unsigned integer (0-127), a note identifier, a [`Note`](Note) object or an array of the previous types, to an array of [`Note`](Note) objects. [`Note`](Note) objects are returned as is. For note numbers and identifiers, a [`Note`](Note) object is created with the options specified. An error will be thrown when encountering invalid input. Note: if both the `attack` and `rawAttack` options are specified, the later has priority. The same goes for `release` and `rawRelease`. **Parameters** > Signature: `buildNoteArray([notes], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`notes`**] | number
string
Note
Array.<number>
Array.<string>
Array.<Note>
||| |[**`options`**] | object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the note should be explicitly stopped.| |[**`options.attack`**] | number
|0.5|The note's attack velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.release`**] | number
|0.5|The note's release velocity as a float between 0 and 1. If you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.| |[**`options.rawAttack`**] | number
|64|The note's attack velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `attack` and `rawAttack` are specified, the latter has precedence.| |[**`options.rawRelease`**] | number
|64|The note's release velocity as an integer between 0 and 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both `release` and `rawRelease` are specified, the latter has precedence.| |[**`options.octaveOffset`**] | number
|0|An integer to offset the octave by. **This is only used when the input value is a note identifier.**|
**Return Value** > Returns: `Array.`
**Attributes**: static **Throws**: * TypeError An element could not be parsed as a note. ### `.from7bitToFloat(...)` {#from7bitToFloat} Returns a number between 0 and 1 representing the ratio of the input value divided by 127 (7 bit). The returned value is restricted between 0 and 1 even if the input is greater than 127 or smaller than 0. Passing `Infinity` will return `1` and passing `-Infinity` will return `0`. Otherwise, when the input value cannot be converted to an integer, the method returns 0. **Parameters** > Signature: `from7bitToFloat(value)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||A positive integer between 0 and 127 (inclusive)|
**Return Value** > Returns: `number`
A number between 0 and 1 (inclusive) **Attributes**: static ### `.fromFloatTo7Bit(...)` {#fromFloatTo7Bit} Returns an integer between 0 and 127 which is the result of multiplying the input value by 127. The input value should be a number between 0 and 1 (inclusively). The returned value is restricted between 0 and 127 even if the input is greater than 1 or smaller than 0. Passing `Infinity` will return `127` and passing `-Infinity` will return `0`. Otherwise, when the input value cannot be converted to a number, the method returns 0. **Parameters** > Signature: `fromFloatTo7Bit(value)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||A positive float between 0 and 1 (inclusive)|
**Return Value** > Returns: `number`
A number between 0 and 127 (inclusive) **Attributes**: static ### `.fromFloatToMsbLsb(...)` {#fromFloatToMsbLsb} Extracts 7bit MSB and LSB values from the supplied float. **Parameters** > Signature: `fromFloatToMsbLsb(value)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | number
||A float between 0 and 1|
**Return Value** > Returns: `Object`
**Attributes**: static ### `.fromMsbLsbToFloat(...)` {#fromMsbLsbToFloat} Combines and converts MSB and LSB values (0-127) to a float between 0 and 1. The returned value is within between 0 and 1 even if the result is greater than 1 or smaller than 0. **Parameters** > Signature: `fromMsbLsbToFloat(msb, [lsb])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`msb`** | number
||The most significant byte as a integer between 0 and 127.| |[**`lsb`**] | number
|0|The least significant byte as a integer between 0 and 127.|
**Return Value** > Returns: `number`
A float between 0 and 1. **Attributes**: static ### `.getCcNameByNumber(...)` {#getCcNameByNumber} Returns the name of a control change message matching the specified number (0-127). Some valid control change numbers do not have a specific name or purpose assigned in the MIDI [spec](https://midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2). In these cases, the method returns `controllerXXX` (where XXX is the number). **Parameters** > Signature: `getCcNameByNumber(number)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`number`** | number
||An integer (0-127) representing the control change message|
**Return Value** > Returns: `string` or `undefined`
The matching control change name or `undefined` if no match was found. **Attributes**: static ### `.getCcNumberByName(...)` {#getCcNumberByName} **Since**: 3.1
Returns the number of a control change message matching the specified name. **Parameters** > Signature: `getCcNumberByName(name)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`name`** | string
||A string representing the control change message|
**Return Value** > Returns: `string` or `undefined`
The matching control change number or `undefined` if no match was found. **Attributes**: static ### `.getChannelModeByNumber(...)` {#getChannelModeByNumber} **Since**: 2.0.0
Returns the channel mode name matching the specified number. If no match is found, the function returns `false`. **Parameters** > Signature: `getChannelModeByNumber(number)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`number`** | number
||An integer representing the channel mode message (120-127)|
**Return Value** > Returns: `string` or `false`
The name of the matching channel mode or `false` if no match could be found. **Attributes**: static ### `.getNoteDetails(...)` {#getNoteDetails} **Since**: 3.0.0
Given a proper note identifier (`C#4`, `Gb-1`, etc.) or a valid MIDI note number (0-127), this method returns an object containing broken down details about the specified note (uppercase letter, accidental and octave). When a number is specified, the translation to note is done using a value of 60 for middle C (C4 = middle C). **Parameters** > Signature: `getNoteDetails(value)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`value`** | string
number
||A note identifier A atring ("C#4", "Gb-1", etc.) or a MIDI note number (0-127).|
**Return Value** > Returns: `Object`
**Attributes**: static **Throws**: * TypeError Invalid note identifier ### `.getPropertyByValue(...)` {#getPropertyByValue} Returns the name of the first property of the supplied object whose value is equal to the one supplied. If nothing is found, `undefined` is returned. **Parameters** > Signature: `getPropertyByValue(object, value)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`object`** | object
||The object to look for the property in.| |**`value`** | *
||Any value that can be expected to be found in the object's properties.|
**Return Value** > Returns: `string` or `undefined`
The name of the matching property or `undefined` if nothing is found. **Attributes**: static ### `.guessNoteNumber(...)` {#guessNoteNumber} **Since**: 3.0.0
Returns a valid MIDI note number (0-127) given the specified input. The input usually is a string containing a note identifier (`"C3"`, `"F#4"`, `"D-2"`, `"G8"`, etc.). If an integer between 0 and 127 is passed, it will simply be returned as is (for convenience). Other strings will be parsed for integer value, if possible. If the input is an identifier, the resulting note number is offset by the `octaveOffset` parameter. For example, if you pass in "C4" (note number 60) and the `octaveOffset` value is -2, the resulting MIDI note number will be 36. **Parameters** > Signature: `guessNoteNumber(input, octaveOffset)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`input`** | string
number
||A string or number to extract the MIDI note number from.| |**`octaveOffset`** | number
||An integer to offset the octave by|
**Return Value** > Returns: `number` or `false`
A valid MIDI note number (0-127) or `false` if the input could not successfully be parsed to a note number. **Attributes**: static ### `.offsetNumber(...)` {#offsetNumber} Returns the supplied MIDI note number offset by the requested octave and semitone values. If the calculated value is less than 0, 0 will be returned. If the calculated value is more than 127, 127 will be returned. If an invalid offset value is supplied, 0 will be used. **Parameters** > Signature: `offsetNumber(number, octaveOffset, octaveOffset)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`number`** | number
||The MIDI note to offset as an integer between 0 and 127.| |**`octaveOffset`** | number
|0|An integer to offset the note by (in octave)| |**`octaveOffset`** | number
||An integer to offset the note by (in semitones)|
**Return Value** > Returns: `number`
An integer between 0 and 127 **Attributes**: static **Throws**: * `Error` : Invalid note number ### `.sanitizeChannels(...)` {#sanitizeChannels} **Since**: 3.0.0
Returns a sanitized array of valid MIDI channel numbers (1-16). The parameter should be a single integer or an array of integers. For backwards-compatibility, passing `undefined` as a parameter to this method results in all channels being returned (1-16). Otherwise, parameters that cannot successfully be parsed to integers between 1 and 16 are silently ignored. **Parameters** > Signature: `sanitizeChannels([channel])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`channel`**] | number
Array.<number>
||An integer or an array of integers to parse as channel numbers.|
**Return Value** > Returns: `Array.`
An array of 0 or more valid MIDI channel numbers. **Attributes**: static ### `.toNoteIdentifier(...)` {#toNoteIdentifier} **Since**: 3.0.0
Returns an identifier string representing a note name (with optional accidental) followed by an octave number. The octave can be offset by using the `octaveOffset` parameter. **Parameters** > Signature: `toNoteIdentifier(number, octaveOffset)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`number`** | number
||The MIDI note number to convert to a note identifier| |**`octaveOffset`** | number
||An offset to apply to the resulting octave|
**Return Value** > Returns: `string`
**Attributes**: static **Throws**: * RangeError Invalid note number * RangeError Invalid octaveOffset value ### `.toNoteNumber(...)` {#toNoteNumber} **Since**: 3.0.0
Returns a MIDI note number matching the identifier passed in the form of a string. The identifier must include the octave number. The identifier also optionally include a sharp (#), a double sharp (##), a flat (b) or a double flat (bb) symbol. For example, these are all valid identifiers: C5, G4, D#-1, F0, Gb7, Eb-1, Abb4, B##6, etc. When converting note identifiers to numbers, C4 is considered to be middle C (MIDI note number 60) as per the scientific pitch notation standard. The resulting note number can be offset by using the `octaveOffset` parameter. **Parameters** > Signature: `toNoteNumber(identifier, [octaveOffset])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`identifier`** | string
||The identifier in the form of a letter, followed by an optional "#", "##", "b" or "bb" followed by the octave number. For exemple: C5, G4, D#-1, F0, Gb7, Eb-1, Abb4, B##6, etc.| |[**`octaveOffset`**] | number
|0|A integer to offset the octave by.|
**Return Value** > Returns: `number`
The MIDI note number (an integer between 0 and 127). **Attributes**: static **Throws**: * RangeError Invalid 'octaveOffset' value * TypeError Invalid note identifier ### `.toTimestamp(...)` {#toTimestamp} **Since**: 3.0.0
Returns a valid timestamp, relative to the navigation start of the document, derived from the `time` parameter. If the parameter is a string starting with the "+" sign and followed by a number, the resulting timestamp will be the sum of the current timestamp plus that number. If the parameter is a positive number, it will be returned as is. Otherwise, false will be returned. **Parameters** > Signature: `toTimestamp([time])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`time`**] | number
string
||The time string (e.g. `"+2000"`) or number to parse|
**Return Value** > Returns: `number` or `false`
A positive number or `false` (if the time cannot be converted) **Attributes**: static ================================================ FILE: website/api/classes/WebMidi.md ================================================ # WebMidi The `WebMidi` object makes it easier to work with the low-level Web MIDI API. Basically, it simplifies sending outgoing MIDI messages and reacting to incoming MIDI messages. When using the WebMidi.js library, you should know that the `WebMidi` class has already been instantiated. You cannot instantiate it yourself. If you use the **IIFE** version, you should simply use the global object called `WebMidi`. If you use the **CJS** (CommonJS) or **ESM** (ES6 module) version, you get an already-instantiated object when you import the module. **Extends**: [`EventEmitter`](EventEmitter) **Fires**: [`connected`](#event:connected), [`disabled`](#event:disabled), [`disconnected`](#event:disconnected), [`enabled`](#event:enabled), [`error`](#event:error), [`midiaccessgranted`](#event:midiaccessgranted), [`portschanged`](#event:portschanged) ### `Constructor` The WebMidi class is a singleton and you cannot instantiate it directly. It has already been instantiated for you. *** ## Properties ### `.defaults` {#defaults} **Type**: object
Object containing system-wide default values that can be changed to customize how the library works. **Properties** | Property | Type | Description | | ------------ | ------------ | ------------ | |**`defaults.note`** |object|Default values relating to note| |**`defaults.note.attack`** |number|A number between 0 and 127 representing the default attack velocity of notes. Initial value is 64.| |**`defaults.note.release`** |number|A number between 0 and 127 representing the default release velocity of notes. Initial value is 64.| |**`defaults.note.duration`** |number|A number representing the default duration of notes (in seconds). Initial value is Infinity.| ### `.enabled` {#enabled} **Type**: boolean
**Attributes**: read-only
Indicates whether access to the host's MIDI subsystem is active or not. ### `.eventCount` {#eventCount} **Type**: number
**Attributes**: read-only
The number of unique events that have registered listeners. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventMap` {#eventMap} **Type**: Object
**Attributes**: read-only
An object containing a property for each event with at least one registered listener. Each event property contains an array of all the [`Listener`](Listener) objects registered for the event. ### `.eventNames` {#eventNames} **Type**: Array.<string>
**Attributes**: read-only
An array of all the unique event names for which the emitter has at least one registered listener. Note: this excludes global events registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) because they are not tied to a specific event. ### `.eventsSuspended` {#eventsSuspended} **Type**: boolean
Whether or not the execution of callbacks is currently suspended for this emitter. ### `.flavour` {#flavour} **Since**: 3.0.25
**Type**: string
**Attributes**: read-only
The flavour of the library. Can be one of: * `esm`: ECMAScript Module * `cjs`: CommonJS Module * `iife`: Immediately-Invoked Function Expression ### `.inputs` {#inputs} **Type**: Array.<Input>
**Attributes**: read-only
An array of all currently available MIDI inputs. ### `.interface` {#interface} **Type**: MIDIAccess
**Attributes**: read-only
The [`MIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) instance used to talk to the lower-level Web MIDI API. This should not be used directly unless you know what you are doing. ### `.octaveOffset` {#octaveOffset} **Since**: 2.1
**Type**: number
An integer to offset the octave of notes received from external devices or sent to external devices. When a MIDI message comes in on an input channel the reported note name will be offset. For example, if the `octaveOffset` is set to `-1` and a [`"noteon"`](InputChannel#event:noteon) message with MIDI number 60 comes in, the note will be reported as C3 (instead of C4). By the same token, when [`OutputChannel.playNote()`](OutputChannel#playNote) is called, the MIDI note number being sent will be offset. If `octaveOffset` is set to `-1`, the MIDI note number sent will be 72 (instead of 60). ### `.outputs` {#outputs} **Type**: Array.<Output>
**Attributes**: read-only
An array of all currently available MIDI outputs as [`Output`](Output) objects. ### `.supported` {#supported} **Type**: boolean
**Attributes**: read-only
Indicates whether the environment provides support for the Web MIDI API or not. **Note**: in environments that do not offer built-in MIDI support, this will report `true` if the [`navigator.requestMIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess) function is available. For example, if you have installed WebMIDIAPIShim.js but no plugin, this property will be `true` even though actual support might not be there. ### `.sysexEnabled` {#sysexEnabled} **Type**: boolean
**Attributes**: read-only
Indicates whether MIDI system exclusive messages have been activated when WebMidi.js was enabled via the [`enable()`](#enable) method. ### `.time` {#time} **Type**: DOMHighResTimeStamp
**Attributes**: read-only
The elapsed time, in milliseconds, since the time [origin](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin). Said simply, it is the number of milliseconds that passed since the page was loaded. Being a floating-point number, it has sub-millisecond accuracy. According to the [documentation](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp), the time should be accurate to 5 µs (microseconds). However, due to various constraints, the browser might only be accurate to one millisecond. Note: `WebMidi.time` is simply an alias to `performance.now()`. ### `.validation` {#validation} **Type**: boolean
Indicates whether argument validation and backwards-compatibility checks are performed throughout the WebMidi.js library for object methods and property setters. This is an advanced setting that should be used carefully. Setting `validation` to `false` improves performance but should only be done once the project has been thoroughly tested with `validation` turned on. ### `.version` {#version} **Type**: string
**Attributes**: read-only
The version of the library as a [semver](https://semver.org/) string. *** ## Methods ### `.addListener(...)` {#addListener} Adds a listener for the specified event. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to.| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs.| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The value of `this` in the callback function.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.remaining`**] | number
|Infinity|The number of times after which the callback should automatically be removed.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.addOneTimeListener(...)` {#addOneTimeListener} Adds a one-time listener for the specified event. The listener will be executed once and then destroyed. It returns the [`Listener`](Listener) object that was created and attached to the event. To attach a global listener that will be triggered for any events, use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. Note that a global listener will also be triggered by non-registered events. **Parameters** > Signature: `addOneTimeListener(event, callback, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to listen to| |**`callback`** | EventEmitter~callback
||The callback function to execute when the event occurs| |[**`options`**] | Object
|{}|| |[**`options.context`**] | Object
|this|The context to invoke the callback function in.| |[**`options.prepend`**] | boolean
|false|Whether the listener should be added at the beginning of the listeners array and thus executed first.| |[**`options.duration`**] | number
|Infinity|The number of milliseconds before the listener automatically expires.| |[**`options.arguments`**] | array
||An array of arguments which will be passed separately to the callback function. This array is stored in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object and can be retrieved or modified as desired.|
**Return Value** > Returns: `Listener`
The newly created [`Listener`](Listener) object. **Throws**: * `TypeError` : The `event` parameter must be a string or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). * `TypeError` : The `callback` parameter must be a function. ### `.disable()` {#disable} **Since**: 2.0.0
**Attributes**: async Completely disables **WebMidi.js** by unlinking the MIDI subsystem's interface and closing all [`Input`](Input) and [`Output`](Output) objects that may have been opened. This also means that listeners added to [`Input`](Input) objects, [`Output`](Output) objects or to `WebMidi` itself are also destroyed. **Return Value** > Returns: `Promise.`
**Throws**: * `Error` : The Web MIDI API is not supported by your environment. ### `.emit(...)` {#emit} Executes the callback function of all the [`Listener`](Listener) objects registered for a given event. The callback functions are passed the additional arguments passed to `emit()` (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of the [`Listener`](Listener) object (if any). If the [`eventsSuspended`](#eventsSuspended) property is `true` or the [`Listener.suspended`](Listener#suspended) property is `true`, the callback functions will not be executed. This function returns an array containing the return values of each of the callbacks. It should be noted that the regular listeners are triggered first followed by the global listeners (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). **Parameters** > Signature: `emit(event, ...args)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
||The event| |**`args`** | *
||Arbitrary number of arguments to pass along to the callback functions|
**Return Value** > Returns: `Array`
An array containing the return value of each of the executed listener functions. **Throws**: * `TypeError` : The `event` parameter must be a string. ### `.enable(...)` {#enable} **Attributes**: async Checks if the Web MIDI API is available in the current environment and then tries to connect to the host's MIDI subsystem. This is an asynchronous operation and it causes a security prompt to be displayed to the user. To enable the use of MIDI system exclusive messages, the `sysex` option should be set to `true`. However, under some environments (e.g. Jazz-Plugin), the `sysex` option is ignored and system exclusive messages are always enabled. You can check the [`sysexEnabled`](#sysexEnabled) property to confirm. To enable access to software synthesizers available on the host, you would set the `software` option to `true`. However, this option is only there to future-proof the library as support for software synths has not yet been implemented in any browser (as of September 2021). By the way, if you call the [`enable()`](#enable) method while WebMidi.js is already enabled, the callback function will be executed (if any), the promise will resolve but the events ([`"midiaccessgranted"`](#event:midiaccessgranted), [`"connected"`](#event:connected) and [`"enabled"`](#event:enabled)) will not be fired. There are 3 ways to execute code after `WebMidi` has been enabled: - Pass a callback function in the `options` - Listen to the [`"enabled"`](#event:enabled) event - Wait for the promise to resolve In order, this is what happens towards the end of the enabling process: 1. [`"midiaccessgranted"`](#event:midiaccessgranted) event is triggered once the user has granted access to use MIDI. 2. [`"connected"`](#event:connected) events are triggered (for each available input and output) 3. [`"enabled"`](#event:enabled) event is triggered when WebMidi.js is fully ready 4. specified callback (if any) is executed 5. promise is resolved and fulfilled with the `WebMidi` object. **Important note**: starting with Chrome v77, a page using Web MIDI API must be hosted on a secure origin (`https://`, `localhost` or `file:///`) and the user will always be prompted to authorize the operation (no matter if the `sysex` option is `true` or not). ##### Example ```js // Enabling WebMidi and using the promise WebMidi.enable().then(() => { console.log("WebMidi.js has been enabled!"); }) ``` **Parameters** > Signature: `enable([options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`options`**] | object
||| |[**`options.callback`**] | function
||A function to execute once the operation completes. This function will receive an `Error` object if enabling the Web MIDI API failed.| |[**`options.sysex`**] | boolean
|false|Whether to enable MIDI system exclusive messages or not.| |[**`options.validation`**] | boolean
|true|Whether to enable library-wide validation of method arguments and setter values. This is an advanced setting that should be used carefully. Setting [`validation`](#validation) to `false` improves performance but should only be done once the project has been thoroughly tested with [`validation`](#validation) turned on.| |[**`options.software`**] | boolean
|false|Whether to request access to software synthesizers on the host system. This is part of the spec but has not yet been implemented by most browsers as of April 2020.| |[**`options.requestMIDIAccessFunction`**] | function
||A custom function to use to return the MIDIAccess object. This is useful if you want to use a polyfill for the Web MIDI API or if you want to use a custom implementation of the Web MIDI API - probably for testing purposes.|
**Return Value** > Returns: `Promise.`
The promise is fulfilled with the `WebMidi` object for chainability **Throws**: * `Error` : The Web MIDI API is not supported in your environment. * `Error` : Jazz-Plugin must be installed to use WebMIDIAPIShim. ### `.getInputById(...)` {#getInputById} **Since**: 2.0.0
Returns the [`Input`](Input) object that matches the specified ID string or `false` if no matching input is found. As per the Web MIDI API specification, IDs are strings (not integers). Please note that IDs change from one host to another. For example, Chrome does not use the same kind of IDs as Jazz-Plugin. **Parameters** > Signature: `getInputById(id, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`id`** | string
||The ID string of the input. IDs can be viewed by looking at the [`WebMidi.inputs`](WebMidi#inputs) array. Even though they sometimes look like integers, IDs are strings.| |[**`options`**] | object
||| |[**`options.disconnected`**] | boolean
||Whether to retrieve a disconnected input|
**Return Value** > Returns: `Input`
An [`Input`](Input) object matching the specified ID string or `undefined` if no matching input can be found. **Throws**: * `Error` : WebMidi is not enabled. ### `.getInputByName(...)` {#getInputByName} **Since**: 2.0.0
Returns the first [`Input`](Input) object whose name **contains** the specified string. Note that the port names change from one environment to another. For example, Chrome does not report input names in the same way as the Jazz-Plugin does. **Parameters** > Signature: `getInputByName(name, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`name`** | string
||The non-empty string to look for within the name of MIDI inputs (such as those visible in the [inputs](WebMidi#inputs) array).| |[**`options`**] | object
||| |[**`options.disconnected`**] | boolean
||Whether to retrieve a disconnected input|
**Return Value** > Returns: `Input`
The [`Input`](Input) that was found or `undefined` if no input contained the specified name. **Throws**: * `Error` : WebMidi is not enabled. ### `.getListenerCount(...)` {#getListenerCount} Returns the number of listeners registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) do not count towards the remaining number for a "regular" event. To get the number of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListenerCount(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event which is usually a string but can also be the special [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) symbol.|
**Return Value** > Returns: `number`
An integer representing the number of listeners registered for the specified event. ### `.getListeners(...)` {#getListeners} Returns an array of all the [`Listener`](Listener) objects that have been registered for a specific event. Please note that global events (those added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)) are not returned for "regular" events. To get the list of global listeners, specifically use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `getListeners(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to get listeners for.|
**Return Value** > Returns: `Array.`
An array of [`Listener`](Listener) objects. ### `.getOutputById(...)` {#getOutputById} **Since**: 2.0.0
Returns the [`Output`](Output) object that matches the specified ID string or `false` if no matching output is found. As per the Web MIDI API specification, IDs are strings (not integers). Please note that IDs change from one host to another. For example, Chrome does not use the same kind of IDs as Jazz-Plugin. **Parameters** > Signature: `getOutputById(id, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`id`** | string
||The ID string of the port. IDs can be viewed by looking at the [`WebMidi.outputs`](WebMidi#outputs) array.| |[**`options`**] | object
||| |[**`options.disconnected`**] | boolean
||Whether to retrieve a disconnected output|
**Return Value** > Returns: `Output`
An [`Output`](Output) object matching the specified ID string. If no matching output can be found, the method returns `undefined`. **Throws**: * `Error` : WebMidi is not enabled. ### `.getOutputByName(...)` {#getOutputByName} **Since**: 2.0.0
Returns the first [`Output`](Output) object whose name **contains** the specified string. Note that the port names change from one environment to another. For example, Chrome does not report input names in the same way as the Jazz-Plugin does. **Parameters** > Signature: `getOutputByName(name, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`name`** | string
||The non-empty string to look for within the name of MIDI inputs (such as those visible in the [`outputs`](#outputs) array).| |[**`options`**] | object
||| |[**`options.disconnected`**] | boolean
||Whether to retrieve a disconnected output|
**Return Value** > Returns: `Output`
The [`Output`](Output) that was found or `undefined` if no output matched the specified name. **Throws**: * `Error` : WebMidi is not enabled. ### `.hasListener(...)` {#hasListener} Returns `true` if the specified event has at least one registered listener. If no event is specified, the method returns `true` if any event has at least one listener registered (this includes global listeners registered to [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT)). Note: to specifically check for global listeners added with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT), use [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the parameter. **Parameters** > Signature: `hasListener([event], [callback])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
Symbol
|(any event)|The event to check| |[**`callback`**] | function
Listener
|(any callback)|The actual function that was added to the event or the [Listener](Listener) object returned by `addListener()`.|
**Return Value** > Returns: `boolean`
### `.removeListener(...)` {#removeListener} Removes all the listeners that were added to the object upon which the method is called and that match the specified criterias. If no parameters are passed, all listeners added to this object will be removed. If only the `event` parameter is passed, all listeners for that event will be removed from that object. You can remove global listeners by using [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) as the first parameter. To use more granular options, you must at least define the `event`. Then, you can specify the callback to match or one or more of the additional options. **Parameters** > Signature: `removeListener([event], [callback], [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |[**`event`**] | string
||The event name.| |[**`callback`**] | EventEmitter~callback
||Only remove the listeners that match this exact callback function.| |[**`options`**] | Object
||| |[**`options.context`**] | *
||Only remove the listeners that have this exact context.| |[**`options.remaining`**] | number
||Only remove the listener if it has exactly that many remaining times to be executed.|
### `.suspendEvent(...)` {#suspendEvent} Suspends execution of all callbacks functions registered for the specified event type. You can suspend execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `suspendEvent()`. Beware that this will not suspend all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive at first glance, it allows the selective suspension of global listeners while leaving other listeners alone. If you truly want to suspends all callbacks for a specific [`EventEmitter`](EventEmitter), simply set its `eventsSuspended` property to `true`. **Parameters** > Signature: `suspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to suspend execution of all callback functions.|
### `.unsuspendEvent(...)` {#unsuspendEvent} Resumes execution of all suspended callback functions registered for the specified event type. You can resume execution of callbacks registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) by passing [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) to `unsuspendEvent()`. Beware that this will not resume all callbacks but only those registered with [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT). While this may seem counter-intuitive, it allows the selective unsuspension of global listeners while leaving other callbacks alone. **Parameters** > Signature: `unsuspendEvent(event)`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event name (or `EventEmitter.ANY_EVENT`) for which to resume execution of all callback functions.|
### `.waitFor(...)` {#waitFor} **Attributes**: async The `waitFor()` method is an async function which returns a promise. The promise is fulfilled when the specified event occurs. The event can be a regular event or [`EventEmitter.ANY_EVENT`](EventEmitter#ANY_EVENT) (if you want to resolve as soon as any event is emitted). If the `duration` option is set, the promise will only be fulfilled if the event is emitted within the specified duration. If the event has not been fulfilled after the specified duration, the promise is rejected. This makes it super easy to wait for an event and timeout after a certain time if the event is not triggered. **Parameters** > Signature: `waitFor(event, [options])`
| Parameter | Type(s) | Default | Description | | ------------ | ------------ | ------------ | ------------ | |**`event`** | string
Symbol
||The event to wait for| |[**`options`**] | Object
|{}|| |[**`options.duration`**] | number
|Infinity|The number of milliseconds to wait before the promise is automatically rejected.|
*** ## Events ### `connected` {#event-connected} Event emitted when an [`Input`](Input) or [`Output`](Output) becomes available. This event is typically fired whenever a MIDI device is plugged in. Please note that it may fire several times if a device possesses multiple inputs and/or outputs (which is often the case). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`connected`| |**`target`** |WebMidi|The object to which the listener was originally added (`WebMidi`)| |**`port`** |Input|The [`Input`](Input) or [`Output`](Output) object that triggered the event.| ### `disabled` {#event-disabled} Event emitted once `WebMidi` has been successfully disabled. **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |DOMHighResTimeStamp|The moment when the event occurred (in milliseconds since the navigation start of the document).| |**`target`** |WebMidi|The object that triggered the event| |**`type`** |string|`"disabled"`| ### `disconnected` {#event-disconnected} Event emitted when an [`Input`](Input) or [`Output`](Output) becomes unavailable. This event is typically fired whenever a MIDI device is unplugged. Please note that it may fire several times if a device possesses multiple inputs and/or outputs (which is often the case). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |DOMHighResTimeStamp|The moment when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`disconnected`| |**`target`** |WebMidi|The object to which the listener was originally added (`WebMidi`)| |**`port`** |Input|The [`Input`](Input) or [`Output`](Output) object that triggered the event.| ### `enabled` {#event-enabled} Event emitted once `WebMidi` has been fully enabled **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |DOMHighResTimeStamp|The moment when the event occurred (in milliseconds since the navigation start of the document).| |**`target`** |WebMidi|The object that triggered the event| |**`type`** |string|`"enabled"`| ### `error` {#event-error} Event emitted when an error occurs trying to enable `WebMidi` **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |DOMHighResTimeStamp|The moment when the event occurred (in milliseconds since the navigation start of the document).| |**`target`** |WebMidi|The object that triggered the event| |**`type`** |string|`error`| |**`error`** |*|Actual error that occurred| ### `midiaccessgranted` {#event-midiaccessgranted} Event emitted once the MIDI interface has been successfully created (which implies user has granted access to MIDI). **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |DOMHighResTimeStamp|The moment when the event occurred (in milliseconds since the navigation start of the document).| |**`target`** |WebMidi|The object that triggered the event| |**`type`** |string|`midiaccessgranted`| ### `portschanged` {#event-portschanged} Event emitted when an [`Input`](Input) or [`Output`](Output) port is connected or disconnected. This event is typically fired whenever a MIDI device is plugged in or unplugged. Please note that it may fire several times if a device possesses multiple inputs and/or outputs (which is often the case). **Since**: 3.0.2 **Event Properties** | Property | Type | Description | | ------------------------ | ------------------------ | ------------------------ | |**`timestamp`** |number|The moment (DOMHighResTimeStamp) when the event occurred (in milliseconds since the navigation start of the document).| |**`type`** |string|`portschanged`| |**`target`** |WebMidi|The object to which the listener was originally added (`WebMidi`)| |**`port`** |Input|The [`Input`](Input) or [`Output`](Output) object that triggered the event.| ================================================ FILE: website/api/classes/_category_.json ================================================ { "label": "Classes & Objects", "position": 2, "collapsed": false } ================================================ FILE: website/api/index.md ================================================ --- sidebar_position: 1 title: API Documentation slug: / --- # API Documentation ## Core Classes These classes are the ones developers are most likely to be dealing with while working on their MIDI projects. Note that all these classes are pre-instantiated within WEBMIDI.js. * [**WebMidi**](./classes/WebMidi.md) * [**Input**](./classes/Input.md) * [**InputChannel**](./classes/InputChannel.md) * [**Output**](./classes/Output.md) * [**OutputChannel**](./classes/OutputChannel.md) * [**Message**](./classes/Message.md) The exception are the [`Note`](./classes/Note.md) class which you can instantiate when you need to store a musical note and the [`Forwarder`](./classes/Forwarder.md) class used to forward messages from an input to an output: * [**Note**](./classes/Note.md) * [**Forwarder**](./classes/Forwarder.md) ## Support Classes These classes are mostly for internal use, but you might find them useful in some contexts. The [`Enumerations`](./classes/Enumerations.md) class contains static enums of MIDI messages, registered parameters, etc. The [`Utilities`](./classes/Utilities.md) class contains various static methods. * [**Enumerations**](./classes/Enumerations.md) * [**Utilities**](./classes/Utilities.md) ## DjipEvents Classes The `EventEmitter` and `Listener` classes from the [DjipEvents](https://github.com/djipco/djipevents) module are extended by various WEBMIDI.js classes. So, in the interest of completeness, we include their full documentation here and cross-reference it with the core classes * [**EventEmitter**](./classes/EventEmitter.md) * [**Listener**](./classes/Listener.md) ================================================ FILE: website/babel.config.js ================================================ module.exports = { presets: [require.resolve("@docusaurus/core/lib/babel/preset")], }; ================================================ FILE: website/blog/2021-12-01/version-3-has-been-released.md ================================================ --- title: WEBMIDI.js v3 is available now! description: Version 3 of WEBMIDI.js, the library that lets you interact with your MIDI instruments and devices, is now available. It features Node.js and TypeScript support, various new objects (Message, Note, etc.) and a completely rewritten engine. authors: - name: Jean-Philippe Côté title: Creator of WEBMIDI.js url: /about image_url: /img/blog/jean-philippe_cote.jpg hide_table_of_contents: false keywords: [web midi api, music, instrument, midi, javascript] image: /img/blog/2021-12-01/webmidijs-is-out.png --- After a lot of work and testing, I am happy to announce today that version 3 of the go-to MIDI library for JavaScript has been released! You can [try it out](https://webmidijs.org/docs) right now! ![](webmidi.js-is-available-now.png) ### About WEBMIDI.js [**WEBMIDI.js**](https://webmidijs.org) exists to make it easier for developers to use the [Web MIDI API](https://webaudio.github.io/web-midi-api/). The Web MIDI API is a really exciting addition to the web platform allowing a web page to interact directly with MIDI musical instruments and devices. While great, many developers will find the API to be too low-level for their needs. Having to perform binary arithmetic or needing to constantly refer to the 300-page MIDI spec is no fun (trust me on this!). So, the goal for [**WEBMIDI.js**](https://webmidijs.org) is to get developers and musicians started with their web-based MIDI projects as efficiently as possible. As of today, [**WEBMIDI.js**](https://webmidijs.org) generates over **744K hits a month on [jsDelivr](https://www.jsdelivr.com/package/npm/webmidi)**. It is **downloaded over 4.4K times a month on [NPM](https://www.npmjs.com/package/webmidi)** and has been **starred by over [1000 developers](https://github.com/djipco/webmidi/stargazers)** on GitHub. Not too bad for a niche library that grew out of a personal passion project. 😀 ### About the New Version 3 Version 3 has been rewritten from scratch to make it both future-proof and backwards-compatible. It uses a modern development paradigm and now has its own dedicated website at [**webmidijs.org**](https://webmidijs.org). The library offers numerous new features such as: * Long-awaited **support for Node.js** (thanks to the [jzz](https://www.npmjs.com/package/jzz) module by Jazz-Soft). The exact same code can be used in supported browsers and in Node.js. * Distribution in **3 flavours**: **ESM** (ECMAScript module for modern browsers), **CJS** (CommonJS module for Node.js) and **IIFE** (Immediately Invoked Function Expression for legacy browsers and _ad hoc_ usage). * **TypeScript Support**. Every new release includes a TypeScript definition file for CJS and ESM in the `dist` directory. * **New `InputChannel` and `OutputChannel`** objects. You can now work with a single MIDI channel if that's appropriate for your needs. * **New `Note` object**. Makes it easier to work with notes and pass them around from one method to the next. * **New `Message` object** that allows easier routing of MIDI messages, including the ability to automatically **forward inbound MIDI messages** to one, or more, outputs (much like the good ol' physical THRU port). * Improved support for **system exclusive** (sysex) messages. * **Support for promises** while preserving legacy callback support. * Improved **support for RPN/NRPN messages**. * Addition of **hundreds of unit tests** to make sure the library remains stable at all times. * and lots more... ### Try it out! The [documentation section](https://webmidijs.org/docs) of the new website has all the information to get you started. If you need help, you can exchange with fellow users and myself using the [GitHub Discussions](https://github.com/djipco/webmidi/discussions) platform. If you use the library and find it useful, please think about [sponsoring](https://github.com/sponsors/djipco) 💜 the project. Cheers! Jean-Philippe ================================================ FILE: website/docs/archives/_category_.json ================================================ { "label": "Previous Versions", "position": 40 } ================================================ FILE: website/docs/archives/v1.md ================================================ --- sidebar_position: 2 slug: /archives/v1 sidebar_label: Version 1.0.0-beta.15 --- # Documentation for v1.0.0-beta.15 :::caution There is no documentation per se for version 1.0.0-beta.15, However, you can still consult an archived copy of the full [API Reference](https://djipco.github.io/webmidi/archives/api/v1/classes/WebMidi.html). ::: ================================================ FILE: website/docs/archives/v2.md ================================================ --- sidebar_position: 1 slug: /archives/v2 sidebar_label: Version 2.5.3 --- # Documentation for v2.5.3 :::caution Version 2.5.3 will be the last version of the 2.x branch. This documentation and the 2.5.3 version will not be updated after 2021. ::: ## Browser Support This library works in all browsers that natively support the [Web MIDI API](https://webaudio.github.io/web-midi-api/). Currently (2021), the following browsers have built-in support: * Chrome (macOS, GNU/Linux, Android & Windows) * Opera (macOS, GNU/Linux, Windows) * Android WebView component (KitKat and above) * Edge (Windows) It is also possible to use this library in other browsers if you install version 1.4+ of [Jazz-Plugin](http://jazz-soft.net/) together with the [WebMIDIAPIShim](http://cwilso.github.io/WebMIDIAPIShim/) polyfill. This combination provides support for the following additional browsers: * Firefox v51 **or less** (Mac, GNU/Linux & Windows) * Safari (macOS) * Internet Explorer (Windows) >For details on how to use **WebMidi.js** with the Jazz-Plugin (and WebMIDIAPIShim, please skip >ahead to the [Using WebMidi.js with the Jazz-Plugin](#using-webmidijs-with-the-jazz-plugin) >section. For **Firefox v52+ support**, you need to install two extensions made by [Jazz-Soft](https://www.jazz-soft.net/): * [Jazz-MIDI extension](https://addons.mozilla.org/en-US/firefox/addon/jazz-midi/) v1.5.1+ * [Web MIDI API extension](https://addons.mozilla.org/en-US/firefox/addon/web-midi-api/) Early tests show that WebMidi.js is working in Firefox when both these extensions installed. Further testing will need to be done but it looks very promising. I invite you to communicate with the Firefox and Safari teams to let them know how having native Web MIDI support is important for you: * Safari: https://bugs.webkit.org/show_bug.cgi?id=107250 * Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=836897 Note that, in 2020, [Apple has announced](https://webkit.org/tracking-prevention/) that they would not implement the Web MIDI API (and a host of other APIs) in Safari over fingerprinting concerns. ## Node.js Support There is no official Node.js support in WebMidi.js 2.5.x. Version 3 and above offer full Node.js support. ## TypeScript Support TypeScript type definitions have been tentatively added to WebMidi.js with version 2.3 (thanks to [mmmveggies](https://www.github.com/mmmveggies)) but it should be noted that **TypeScript IS NOT officially supported** in version 2.5.x. TypeScript is supported in version 3 and above. Despite the absence of official suuport, this has been reported to work in v2.5.3: ```ts import WebMidi from "webmidi"; WebMidi.enable(...); ``` Or (thanks to [michaelcaterisano](https://www.github.com/michaelcaterisano)): ```ts const WebMidi: import("webmidi").WebMidi = require("webmidi"); ``` You can also import the types, if you need them: ```ts import WebMidi, { InputEventNoteon, InputEventNoteoff } from "webmidi"; input.addListener("noteon", "all", (event: InputEventNoteon) => { ... }) ``` ## Installation Depending on your needs and environment, you can install **WebMidi.js** in a variety of different ways. #### CDN The easiest way to get started is to link the WebMidi.js library from the [jsDelivr](https://www.jsdelivr.com/) CDN (content delivery network). To retrieve the latest version, just add this ` #### Manual Install Obviously, you can also install **WebMidi.js** the old fashioned way by downloading the [2.5.3 release](https://github.com/djipco/webmidi/releases/tag/2.5.3) packaged as a zip file. Uncompress the package, grab the `webmidi.min.js` file and copy it to your project. Link to it from your HTML page as usual. #### NPM Install If it's more convenient, you can install **WebMidi.js** with NPM. Simply issue the following command to perform the actual install: npm install webmidi Then, just add a ` #### Using with a Bundler If you are using a bundler such as WebPack, you can import **WebMidi.js** in your project in this way: import WebMidi from 'path/to/webmidi'; ## Insecure Origins Starting with version 77, [Chrome deprecates Web MIDI usage on insecure origins](https://www.chromestatus.com/feature/5138066234671104). This means that, going forward, the page will need to be hosted on a secure origin (e.g. `https://`, `localhost:` or `file:///`) and the user will need to explicitely authorize usage (no matter if `sysex` is used or not). ## Quick Start Getting started is easy. The first thing to do is to enable **WebMidi.js**. To do that, you call `WebMidi.enable()` and pass it a function to execute when done. This function will receive an `Error` object if enabling `WebMidi` failed: ```javascript WebMidi.enable(function (err) { if (err) { console.log("WebMidi could not be enabled.", err); } else { console.log("WebMidi enabled!"); } }); ``` To send and receive MIDI messages, you will need to do so via the appropriate `Output` and `Input` device. To view all the available `Input` and `Output` ports, you can use the matching arrays: ```javascript WebMidi.enable(function (err) { console.log(WebMidi.inputs); console.log(WebMidi.outputs); }); ``` To send MIDI messages to a device, you simply need to grab that device and call one of its output method (`playNote()`, `stopNote()`, `sendPitchBend()`, etc.). To retrieve a device, you can use its position in the `WebMidi.outputs` array. For instance, to grab the first output device, you could use: ```javascript var output = WebMidi.outputs[0]; ``` However, this is not very safe as the position of devices in the array could change. An alternative is to use the device's ID: ```javascript var output = WebMidi.getOutputById("1584982307"); ``` Beware that device IDs are not the same across browsers and platforms. You could also use the device's name (as displayed in the `WebMidi.outputs` array): ```javascript var output = WebMidi.getOutputByName("Axiom Pro 25 Ext Out"); ``` Then, you can call any of the output methods and all native MIDI communications will be handled for you. For example, to play a "C" on the 3rd octave, you simply do: ```javascript output.playNote("C3"); ``` That's it. Receiving messages works in a similar way: you retrieve the `Input` device you want to use, and then add a callback function to be triggered when a specific MIDI message is received. For example, to listen for pitch bend events on all channels of the device: ```javascript var input = WebMidi.getInputByName("Axiom Pro 25 USB A In"); input.addListener('pitchbend', "all", function(e) { console.log("Pitch value: " + e.value); }); ``` ## API Documentation The [API for WebMidi.js](https://webmidijs.org/archives/api/v2/) is fully documented. If you spot an error (even something minor) or think a topic should be made clearer, do not hesitate to [file an issue](https://github.com/djipco/webmidi/issues) or, better yet, send a PR. Here is a link to the full **[API Reference](https://webmidijs.org/archives/api/v2/)**. ## More code examples Here are various other examples to give you an idea of what is possible with **WebMidi.js**. ```javascript // Enable WebMidi.js WebMidi.enable(function (err) { if (err) { console.log("WebMidi could not be enabled.", err); } // Viewing available inputs and outputs console.log(WebMidi.inputs); console.log(WebMidi.outputs); // Reacting when a new device becomes available WebMidi.addListener("connected", function(e) { console.log(e); }); // Reacting when a device becomes unavailable WebMidi.addListener("disconnected", function(e) { console.log(e); }); // Display the current time console.log(WebMidi.time); // Retrieving an output port/device using its id, name or index var output = WebMidi.getOutputById("123456789"); output = WebMidi.getOutputByName("Axiom Pro 25 Ext Out"); output = WebMidi.outputs[0]; // Play a note on all channels of the selected output output.playNote("C3"); // Play a note on channel 3 output.playNote("Gb4", 3); // Play a chord on all available channels output.playNote(["C3", "D#3", "G3"]); // Play a chord on channel 7 output.playNote(["C3", "D#3", "G3"], 7); // Play a note at full velocity on all channels) output.playNote("F#-1", "all", {velocity: 1}); // Play a note on channel 16 in 2 seconds (relative time) output.playNote("F5", 16, {time: "+2000"}); // Play a note on channel 1 at an absolute time in the future output.playNote("F5", 16, {time: WebMidi.time + 3000}); // Play a note for a duration of 2 seconds (will send a note off message in 2 seconds). Also use // a low attack velocity output.playNote("Gb2", 10, {duration: 2000, velocity: 0.25}); // Stop a playing note on all channels output.stopNote("C-1"); // Stopping a playing note on channel 11 output.stopNote("F3", 11); // Stop a playing note on channel 11 and use a high release velocity output.stopNote("G8", 11, {velocity: 0.9}); // Stopping a playing note in 2.5 seconds output.stopNote("Bb2", 11, {time: "+2500"}); // Send polyphonic aftertouch message to channel 8 output.sendKeyAftertouch("C#3", 8, 0.25); // Send pitch bend (between -1 and 1) to channel 12 output.sendPitchBend(-1, 12); // You can chain most method calls output.playNote("G5", 12) .sendPitchBend(-0.5, 12, {time: 400}) // After 400 ms. .sendPitchBend(0.5, 12, {time: 800}) // After 800 ms. .stopNote("G5", 12, {time: 1200}); // After 1.2 s. // Retrieve an input by name, id or index var input = WebMidi.getInputByName("nanoKEY2 KEYBOARD"); input = WebMidi.getInputById("1809568182"); input = WebMidi.inputs[0]; // Listen for a 'note on' message on all channels input.addListener('noteon', "all", function (e) { console.log("Received 'noteon' message (" + e.note.name + e.note.octave + ")."); } ); // Listen to pitch bend message on channel 3 input.addListener('pitchbend', 3, function (e) { console.log("Received 'pitchbend' message.", e); } ); // Listen to control change message on all channels input.addListener('controlchange', "all", function (e) { console.log("Received 'controlchange' message.", e); } ); // Listen to NRPN message on all channels input.addListener('nrpn', "all", function (e) { if(e.controller.type === 'entry') { console.log("Received 'nrpn' 'entry' message.", e); } if(e.controller.type === 'decrement') { console.log("Received 'nrpn' 'decrement' message.", e); } if(e.controller.type === 'increment') { console.log("Received 'nrpn' 'increment' message.", e); } console.log("message value: " + e.controller.value + ".", e); } ); // Check for the presence of an event listener (in such cases, you cannot use anonymous functions). function test(e) { console.log(e); } input.addListener('programchange', 12, test); console.log("Has event listener: ", input.hasListener('programchange', 12, test)); // Remove a specific listener input.removeListener('programchange', 12, test); console.log("Has event listener: ", input.hasListener('programchange', 12, test)); // Remove all listeners of a specific type on a specific channel input.removeListener('noteoff', 12); // Remove all listeners for 'noteoff' on all channels input.removeListener('noteoff'); // Remove all listeners on the input input.removeListener(); }); ``` ## About Sysex Support Per the [Web MIDI API specification](https://webaudio.github.io/web-midi-api/#dom-navigator-requestmidiaccess), system exclusive (sysex) support is disabled by default. If you need to use sysex messages, you will need to pass `true` as the second parameter to `WebMidi.enable()`: ```javascript WebMidi.enable(function (err) { if (err) { console.warn(err); } else { console.log("Sysex is enabled!"); } }, true); ``` **Important**: depending on the browser, version and platform, it may also be necessary to serve the page over https if you want to enable sysex support. ## Migration Notes If you are upgrading from version 1.x to 2.x, you should know that v2.x is not backwards compatible. Some important changes were made to the API to make it easier to use, more versatile and to better future-proof it. Here is a summary of the changes: * All the "output" functions (`playNote()`, `sendPitchBend()`, etc.) have been moved to the `Output` object. A list of all available `Output` objects is available in `WebMidi.outputs` (like before). * All the "input" functions (`addListener`, `removeListener()` and `hasListener()` have been moved to the `Input` object. A list of all available `Input` objects is available in `WebMidi.inputs` (also like before). There might be a few other minor changes here and there but the refactoring mostly concerns the introduction of `Input` and `Output` objects. ## Using WebMidi.js with the Jazz-Plugin To use **WebMidi.js** on Safari, Firefox and Internet Explorer, you will first need to install Jazz-Plugin. Simply [download the plugin](http://jazz-soft.net/download/Jazz-Plugin/) and run the installer. > Users of Firefox v52+ are currently out of luck because Mozilla deactivated support for NPAPI > plugins. There is an add-on version of > [Jazz-Midi](https://addons.mozilla.org/en-US/firefox/addon/jazz-midi/) but, unfortunately, the > API is different and cannot be used as is. Firefox v52+ users will have to wait for native Web > MIDI support to be finalized. > [Reading from the comments on Bug 836897](https://bugzilla.mozilla.org/show_bug.cgi?id=836897), > this might take a while... Then, you will need to add the plugin to the page with the following HTML code: To support recent versions of Internet Explorer, you also need to add a `meta` tag to the `` of the page: Since Jazz-Plugin does not use the same syntax as the native Web MIDI API, it is necessary to also install the [WebMIDIAPIShim](http://cwilso.github.io/WebMIDIAPIShim/) polyfill. You can do that by including the following in your page: Obviously, you can also [download a local copy](https://github.com/cwilso/WebMIDIAPIShim/zipball/master) and link to it. ================================================ FILE: website/docs/getting-started/_category_.json ================================================ { "label": "Getting Started", "position": 10 } ================================================ FILE: website/docs/getting-started/basics.md ================================================ --- sidebar_position: 3 --- # Basics ## Enabling the Library The first step to get started is to enable the library. To do that, you simply call [`WebMidi.enable()`](/api/classes/WebMidi#enable). Starting with v3, the `enable()` method returns a promise which is resolved when the library has been enabled: ```javascript WebMidi .enable() .then(() => console.log("WebMidi enabled!")) .catch(err => alert(err)); ``` :::caution If you intend to use MIDI **system exclusive** messages, you must explicitly enable them by setting the `sysex` option to `true`: ```javascript WebMidi .enable({sysex: true}) .then(() => console.log("WebMidi with sysex enabled!")) .catch(err => alert(err)); ``` ::: ## Listing Available Devices To interact with devices you need to know which `Input` and `Output` ports are available. Connect a MIDI device and try the following: ```javascript WebMidi .enable() .then(onEnabled) .catch(err => alert(err)); function onEnabled() { // Inputs WebMidi.inputs.forEach(input => console.log(input.manufacturer, input.name)); // Outputs WebMidi.outputs.forEach(output => console.log(output.manufacturer, output.name)); } ``` You should see your hardware and software devices appear in the console. Note that many devices make available several input and/or output ports. You can retrieve a reference to an [`Input`](/api/classes/Input) by using the [`getInputByName()`](/api/classes/WebMidi#getInputByName) or [`getInputById()`](/api/classes/WebMidi#getInputById) methods: ```javascript const myInput = WebMidi.getInputByName("MPK mini 3"); ``` Once you have a reference to the input, you can add listeners that will react when a message (such as a note press) arrives. ## Listening For Incoming MIDI Messages On a MIDI device, an input has 16 discrete channels. If you want to listen on all of them, you can add a listener directly on the [`Input`](/api/classes/Input) object: ```javascript const myInput = WebMidi.getInputByName("MPK mini 3"); myInput.addListener("noteon", e => { console.log(e.note.identifier); }) ``` Try playing a note on your device. You should see the note's name and octave in the console. Obviously, you can listen to many more messages coming from your device. For a full list, check out the [`Input.addListener()`](/api/classes/Input#addListener) documentation. It is also possible to listen to messages coming from a specific MIDI channel. For example, when I press the drum pads on my Akai MPK Mini, the messages are sent to channel 10: ```javascript const myInput = WebMidi.getInputByName("MPK mini 3"); const mySynth = myInput.channels[10]; // <-- the MIDI channel (10) mySynth.addListener("noteon", e => { console.log(e.note.identifier, e.message.channel); }) ``` In this case, the listener only listens to **noteon** messages coming in from channel 10 of the input device. ## Sending Outgoing MIDI Messages To send messages to an external device, you must first get a reference to it. For that, you can use methods such as [`getOutputByName()`](/api/classes/WebMidi#getOutputByName) or [`getOutputById()`](/api/classes/WebMidi#getOutputById): ```javascript const myOutput = WebMidi.getOutputByName("SP-404MKII"); ``` Then, you can use various methods to send your message. For example, if you want to tell you sampler to turn all sounds off, you could do the following: ```javascript const myOutput = WebMidi.getOutputByName("SP-404MKII"); myOutput.sendAllSoundOff(); ``` You can learn about all the the methods available to send data by looking at the documentation for the [`Output`](/api/classes/Output) object. You can send messages to a specific MIDI channel by first grabbing a reference to the channel you want. For example, on the Roland SP-404 MK II sampler, you can control a vocoder effet by sending a **pitchbend** message on channel 11: ```javascript const myOutput = WebMidi.getOutputByName("SP-404MKII"); const vocoder = myOutput.channels[11]; vocoder.sendPitchBend(-0.5); ``` In this case, the `vocoder` constant contains an [`OutputChannel`](/api/classes/OutputChannel) object. ## Code Examples Here are various other examples to give you an idea of what is possible with the library. All the examples below only work if the library has first been properly enabled with [`WebMidi.enable()`](/api/classes/WebMidi#enable). ### Retrieve an output port/device using its id, name or array index Different ways to retrieve an output. Beware that IDs are different from one platform to another and on Node.js the `id` is the same as the `name`. ```javascript let output1 = WebMidi.getOutputById("123456789"); let output2 = WebMidi.getOutputByName("Axiom Pro 25 Ext Out"); let output3 = WebMidi.outputs[0]; ``` ### Play a note on a specific MIDI channel (1) The `channels` property of an `Output` object contains references to 16 `OutputChannel` objects (1-16). ```javascript let output = WebMidi.outputs[0]; let channel = output.channels[1]; channel.playNote("C3"); ``` ### Play a note on multiple channels at once You can call [`playNote()`](/api/classes/Output#playNote) (and various other methods) directly on the [`Output`](/api/classes/Output) object. This allows you to play a note on several channels at once. For example, to play a note on channels 1, 2 and 3: ```javascript let output = WebMidi.outputs[0]; output.playNote("Gb4", [1, 2, 3]); ``` You can also create a [`Note`](/api/classes/Note) object and pass it to the [`playNote()`](/api/classes/Output#playNote) method: ```javascript const note = new Note("A4"); const output = WebMidi.outputs[0]; output.playNote(note); ``` ### Play a note on a specific MIDI channel To play a note on a specific MIDI channel, you can use the [`playNote()`](/api/classes/OutputChannel#playNote) method of the [`OutputChannel`](/api/classes/OutputChannel) object (instead of the one on the [`Output`](/api/classes/Output) object). For example, to play a chord on MIDI channel 1: ```javascript let output = WebMidi.outputs[0]; let channel = output.channels[1]; channel.playNote(["C3", "D#3", "G3"]); ``` ### Control note velocity You can control attack and release velocities when playing a note by using the `options` parameter. ```javascript let output = WebMidi.outputs[0]; let channel = output.channels[1]; channel.playNote("C3", {attack: 0.5}); ``` If you prefer to use raw (7 bit) values between 0 and 127, you can use the `rawAttack` option instead: ```javascript let output = WebMidi.outputs[0]; let channel = output.channels[1]; channel.playNote("C3", {rawAttack: 123}); ``` ### Specify note duration If you specify a duration (in decimal milliseconds) for the note, it will automatically be stopped after the duration has expired. For example, to stop it after 1 second (1000 ms): ```javascript let output = WebMidi.outputs[0]; let channel = output.channels[1]; channel.playNote("C3", {duration: 1000}); ``` ### Schedule notes You can specify an absolute or relative time to schedule note playback in the future: ```javascript WebMidi.outputs[0].channels[1].playNote("C3", {time: WebMidi.time + 3000}); WebMidi.outputs[0].channels[1].playNote("C3", {time: "+2000"}); ``` You can retrieve the current time with [`WebMidi.time`](/api/classes/WebMidi#time). The time is in milliseconds (decimal) relative to the navigation start of the document. ### Manually stopping playback You can stop playback of a note right away or in the future. ```javascript WebMidi.outputs[0].channels[1].stopNote("C3"); WebMidi.outputs[0].channels[1].stopNote("C3", {time: "+2500"}); ``` ### Sending a control change (a.k.a. CC) message There are various ways to send a control change message. The most common way is to send the message to a single channel. The first parameter is the controller, the second number is the value: ```javascript // Use controller number WebMidi.outputs[0].channels[1].sendControlChange(72, 64); // Use controller name WebMidi.outputs[0].channels[1].sendControlChange("volumecoarse", 123); ``` As you can see above, you can use either a name or number (0-127) to identify the controller to target. A [list of controller names](https://webmidijs.org/api/classes/OutputChannel#sendControlChange) can be found in the API reference. You can also send the control change message to several channels at once by using the `sendControlChange()` method of the `Output` object: ```javascript // Send to channels 1 through 3 WebMidi.outputs[0].sendControlChange("pancoarse", 123, {channels: [1, 2, 3]}); // Send to all channels WebMidi.outputs[0].sendControlChange(72, 56); ``` ### Set polyphonic aftertouch Send polyphonic aftertouch message to channel 8: ```javascript WebMidi.outputs[0].channels[8].sendKeyAftertouch("B#3", 0.25); ``` ### Set pitch bend value The value is between -1 and 1 (a value of 0 means no bend). ```javascript WebMidi.outputs[0].channels[8].sendPitchBend(-0.25); ``` You can set the range of the bend with [`OutputChannel.sendPitchBendRange()`](/api/classes/OutputChannel#sendPitchBendRange). ### Use Chained Methods Most methods return `this` so you can chain them: ```javascript WebMidi.outputs[0].channels[8] .sendPitchBend(-0.25) .playNote("F4"); ``` ### Listen to event on single channel ```javascript WebMidi.inputs[0].channels[1].addListener("noteon", e => { console.log(`Received 'noteon' message (${e.note.name}${e.note.octave}).`); }); ``` ### Listen to event on multiple channels at once If you add a listener to the `Input` instead of the `InputChannel`, you can listen on multiple channels at once by using the `channels` option: ```javascript WebMidi.inputs[0].addListener("controlchange", e => { console.log(`Received 'controlchange' message.`, e); }, {channels: [1, 2, 3]}); ``` If you do not specify a `channels` option, the listener will listen on all channels. ### Check for the presence of an event listener ```javascript let channel = WebMidi.inputs[0].channels[1]; let test = e => console.log(e); channel.addListener('programchange', test); console.log("Has event listener: ", channel.hasListener('programchange', test)); ``` ### Remove listeners ```javascript let channel = WebMidi.inputs[0].channels[1]; let test = e => console.log(e); channel.removeListener("noteoff", test); // specifically this one channel.removeListener("noteoff"); // all noteoff on this channel channel.removeListener(); // all listeners ``` ## What's Next I hope this short guide helped you getting started. Obviously, the library can do a whole lot more. Some of that is covered in the **Going Further** section but all of it is detailed in the [API documentation](../../api). If you need help, you can ask questions in the [Forum](https://github.com/djipco/webmidi/discussions). If you want to stay posted, I suggest you subscribe to our low-volume [newsletter](https://mailchi.mp/eeffe50651bd/webmidijs-newsletter) and follow our [@webmidijs](https://twitter.com/webmidijs) account on Twitter. Finally, if this software proves useful I cannot encourage you enough to support it by becoming a [sponsor](https://github.com/sponsors/djipco) on GitHub. -- [Jean-Philippe](../../about#who-created-this) ================================================ FILE: website/docs/getting-started/installation.md ================================================ --- sidebar_position: 2 --- # Installation ## Distribution Flavours To cater to various needs, WEBMIDI.js is distributed in 3 different flavours: * **Immediately Invoked Function Expression** (IIFE): This version adds its objects directly to the global namespace. This is the legacy approach which is often easier for beginners. * **ES6 Module** (ESM): This is the modern approach which allows you to `import` the objects as needed (works in newer versions of browsers and Node.js). * **CommonJS Module** (CJS): this is the flavour traditionnally used by Node.js and often with bundling tools such as WebPack. All 3 flavours come in full and minified versions with sourcemap. ## Retrieving the Library Depending on your needs and environment, you can retrieve and install **WEBMIDI.js** in a variety of different ways. Let's look at all of them. ## Linking From CDN The fastest way to get started is to link the library directly from the [jsDelivr](https://www.jsdelivr.com/package/npm/webmidi) CDN (Content Delivery Network). Just add a ` ``` You can retrieve different versions and flavours of the library by modifying the URL. For example, to grab a different flavour replace `/dist/iife/webmidi.iife.js` by one of these: * `/dist/cjs/webmidi.cjs.js` * `/dist/cjs/webmidi.cjs.min.js` * `/dist/esm/webmidi.esm.js` * `/dist/esm/webmidi.esm.min.js` * `/dist/iife/webmidi.iife.js` * `/dist/iife/webmidi.iife.min.js` If you want more control over versions and flavours, check out the [jsDelivr examples](https://www.jsdelivr.com/features). ## Installing Manually Obviously, you can also install the library the old-fashioned way by manually [downloading it](https://cdn.jsdelivr.net/npm/webmidi@latest/dist/iife/webmidi.iife.min.js) and placing it somewhere in your project. Link to it from your HTML page using a ` ``` * ### CommonJS Using **CommonJS** is the traditional approach for Node.js. ```javascript const {WebMidi} = require("webmidi"); ``` * ### ES Module This is the modern approach using the [ECMAScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) format. You can use it in browsers (see [compatibility](https://caniuse.com/es6-module-dynamic-import)) and in Node.js v12+. **Going forward, this is the favoured approach.** **Browsers:** ```javascript import {WebMidi} from "./node_modules/webmidi/dist/esm/webmidi.esm.min.js"; ``` **Node.js:** ```javascript import {WebMidi} from "webmidi"; ``` :::caution ## Insecure Origins Starting with version 77, [Chrome deprecated Web MIDI usage on insecure origins](https://www.chromestatus.com/feature/5138066234671104). This means that, going forward, any page using the library will need to be hosted on a secure origin: * `https://` * `localhost:` * `file:///` Also, the user will need to explicitely authorize usage via a prompt (no matter if system exclusive messages are used or not). ::: ================================================ FILE: website/docs/getting-started/supported-environments.md ================================================ --- sidebar_position: 1 slug: /getting-started --- # Supported Environments Starting with version 3, the library works in both the browser and Node.js. Let's quickly look at the specificities of both these environments. ## Browser Support The library works in all browsers that natively [support](https://caniuse.com/midi) the [Web MIDI API](https://webaudio.github.io/web-midi-api/). Currently, the following major browsers have native support: * Edge v79+ * Chrome 43+ * Opera 30+ * Firefox 108+ It is also possible to use this library in other browsers if you install [Jazz-Plugin](https://jazz-soft.net/download/Jazz-Plugin/) v1.4+. This combination provides support for the following additional web browsers: * Safari * Internet Explorer Note that, in 2020, [Apple has announced](https://webkit.org/tracking-prevention/) that they would not natively support the Web MIDI API (and a host of other APIs) in Safari because of fingerprinting concerns. ## Node.js Support Version 3.0 of WEBMIDI.js introduced full Node.js support. Nothing special needs to be done, it should just work in the following environments (with Node.js 8.5+): * GNU/Linux * macOS * Windows * Raspberry Pi Support for the Node.js environment has been made possible in large part by the good folks of [Jazz-Soft](https://jazz-soft.net/) via their [JZZ](https://www.npmjs.com/package/jzz) module. ## TypeScript Support Starting with version 3, TypeScript is officially supported. You will find the TypeScript definition file in these locations in side tje library's folder: * `/dist/cjs/webmidi.cjs.d.ts` (Node.js module) * `/dist/esm/webmidi.esm.d.ts` (ECMAScript module) ================================================ FILE: website/docs/going-further/_category_.json ================================================ { "label": "Going Further", "position": 20 } ================================================ FILE: website/docs/going-further/electron.md ================================================ --- sidebar_position: 5 title: Electron --- # Electron WEBMIDI.js works fine inside [Electron](https://www.electronjs.org/) but you must make sure to properly handle the permission request and permission check handlers from within the main process: ```javascript mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback, details) => { if (permission === 'midi' || permission === 'midiSysex') { callback(true); } else { callback(false); } }) mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin) => { if (permission === 'midi' || permission === 'midiSysex') { return true; } return false; }); ``` ================================================ FILE: website/docs/going-further/forwarding.md ================================================ --- sidebar_position: 3 title: Forwarding --- # Forwarding Messages Starting with version 3, it is now possible to forward messages from an [`Input`](/api/classes/Input) to an [`Output`](/api/classes/Output). This is done by using the `forward` method of the [`Input`](/api/classes/Input) object. ================================================ FILE: website/docs/going-further/middle-c.md ================================================ --- sidebar_position: 1 --- # Middle C & Octave Offset ## Default Value for Middle C The general MIDI 1.0 specification does not explicitly define a pitch for middle C but it does consider middle C to be note number 60. The **MIDI Tuning Standard** states that note number 69 should be tuned at 440Hz by default, which would make middle C (60) to be C4. However, different manufacturers have assigned middle C to various octaves/pitches (usually C3, C4 or C5). In accordance with the **MIDI Tuning Standard** and the [**scientific pitch notation**](https://en.wikipedia.org/wiki/Scientific_pitch_notation), WEBMIDI.js considers middle C (261.626 Hz) to be C4 by default. ## Offsetting Middle C You can offset the reported note name and octave by using the `octaveOffset` property of various objects. This will make it easier to interface with devices that do not place middle C at C4. ### Inbound Note Example If your external MIDI keyboard sends C3 and WEBMIDI.js reports it as C4, it is because your keyboard places middle C one octave lower than WEBMIDI.js does. To account for that, simply set [`WebMidi.octaveOffset`](/api/classes/WebMidi#octaveOffset) to `-1`. This way, when your keyboard sends C3, WEBMIDI.js will also report it as C3. In both cases the actual note number (60) remains the same. It is just being reported differently. ### Outbound Note Example If you are sending F#4 to an external device and that device thinks it's receiving F#5, it means that the external device places middle C one octave higher. In this case, set [`WebMidi.octaveOffset`](/api/classes/WebMidi#octaveOffset) to `1` to account for the difference. ## Offsetting Granularity For most scenarios, setting the global [`WebMidi.octaveOffset`](/api/classes/WebMidi#octaveOffset) is enough. However, the `octaveOffset` property is available for several objects to allow for better granularity: * [Input](/api/classes/Input) * [InputChannel](/api/classes/InputChannel) * [Output](/api/classes/Output) * [OutputChannel](/api/classes/OutputChannel) * [Note](/api/classes/Note) * [WebMidi](/api/classes/WebMidi) If you define `octaveOffset` on several objects, their value will be added. For example, if you set [`WebMidi.octaveOffset`](/api/classes/WebMidi#octaveOffset) to `-1` and set `octaveOffset` on a specific channel to `1`, the resulting offset on that channel will be `0` (-1 + 1) while the offset on other channels will be `1`. ================================================ FILE: website/docs/going-further/performance.md ================================================ --- sidebar_position: 1 --- # Performance ## Targeting Channels To complete. ## Disabling Validation To complete. ================================================ FILE: website/docs/going-further/sysex.md ================================================ --- sidebar_position: 3 title: Sysex --- # System Exclusive Messages (sysex) ================================================ FILE: website/docs/going-further/typescript.md ================================================ --- sidebar_position: 4 title: TypeScript --- # TypeScript TypeScript is supported in version 3+. However, it has not yet been tested extensively and some minor issues may remain. For instance, some types have been defined as `any` for lack of a better option. One such example has been described in [issue 229](https://github.com/djipco/webmidi/issues/229#issuecomment-1039036353). If you are a TypeScript expert, perhaps you can help. ================================================ FILE: website/docs/index.md ================================================ --- sidebar_position: 1 slug: / --- # Quick Start For v3.x **You want to get started as quickly as possible?** This guide will let you establish a connection with your MIDI instrument in less than 5 minutes. :::info Documentation for [version 2.5.x](https://webmidijs.org/archives/api/v2/) and [version 1.0.0](http://webmidijs.org/archives/api/v1/classes/WebMidi.html) is also available. ::: ## Step 1 - Create the HTML page :::tip Hint: You can **go even faster** by copying the [code](https://github.com/djipco/webmidi/blob/develop/examples/quick-start/index.html) from our GitHub repo. ::: Create an HTML document and link to the library: ```html WebMidi.js Quick Start

WebMidi.js Quick Start

``` ## Step 2 - Add a script Add the following ` ``` ## Step 3 - Connect your device 🎹 Connect an input MIDI device (synth, drum machine, controller, etc.) and load the HTML page in a [compatible browser](/docs/getting-started#browser-support). You will be prompted to authorize the MIDI connection. After authorization, the page should detect the connected MIDI devices and display their name. :::info If nothing shows up, first make sure your MIDI device is detected at the operating system level. ::: ## Step 4 - Listen for MIDI messages In the `onEnabled()` function, we first retrieve the input device we want to work with and store it in the `mySynth` variable. You can retrieve it by number or by name (as you wish). Then we use the `addListener()` method on MIDI channel 1 of the input device to add a callback function that will be called each time a **noteon** event is received on that MIDI channel. ```javascript function onEnabled() { if (WebMidi.inputs.length < 1) { document.body.innerHTML+= "No device detected."; } else { WebMidi.inputs.forEach((device, index) => { document.body.innerHTML+= `${index}: ${device.name}
`; }); } const mySynth = WebMidi.inputs[0]; // const mySynth = WebMidi.getInputByName("TYPE NAME HERE!") mySynth.channels[1].addListener("noteon", e => { document.body.innerHTML+= `${e.note.name}
`; }); } ``` Alternatively, if you wish to listen for notes from several channels at once, you can add the listener directly on the input device itself: ```javascript // Listen to 'note on' events on channels 1, 2 and 3 of the first input MIDI device WebMidi.inputs[0].addListener("noteon", e => { document.body.innerHTML+= `${e.note.name}
`; }, {channels: [1, 2, 3]}); ``` ## Step 5 - Have fun! **That's it!** To go further, please take some time to check out the [Getting Started](getting-started) section. It covers important topics such as installation options, compatibility, security, etc. :::tip If you ever need further help, you can also head over to the [GitHub Discussions](https://github.com/djipco/webmidi/discussions) page and ask all the questions you want! ::: ================================================ FILE: website/docs/migration/_category_.json ================================================ { "label": "Migration", "position": 30 } ================================================ FILE: website/docs/migration/migration.md ================================================ --- sidebar_position: 1 --- # Migrating from 2.5.x to 3.x :::caution This document is a work in progress. Your feedback is critical in identifying and, hopefully, mitigating migration pitfalls. 🙏🏻 Please **share your observations, suggestions and issues** in the [**Migration**](https://github.com/djipco/webmidi/discussions/categories/migration) section of the forum so they can benefit others! ::: ## Highlights of the New Version Version 3.x is a **major** update. It moves WEBMIDI.js from a smallish hobby project to a more serious long-term endeavour. Here are some key highlights: - Modern architecture with **ESM** (ECMAScript module), **IIFE** (Immediately Invoked Function Expression) and **CJS** (CommonJS) flavours - Full **Node.js** support - **TypeScript** definitions files - Various **new objects**: - [`InputChannel`](/api/classes/InputChannel) and [`OutputChannel`](/api/classes/OutputChannel) objects to communicate with a single MIDI channel - [`Note`](/api/classes/Note) object to store and pass around note information - [`Message`](/api/classes/Message) object to better encapsulate MIDI messages - [`Forwarder`](/api/classes/Forwarder) object to allow message forwarding from an input to an output - and others... - More and better **unit tests** to prevent regression issues - Support for **promises** where appropriate (such as [`WebMidi.enable()`](/api/classes/WebMidi#enable)) - More granular **events**. For example: - `midiaccessgranted` to know when a user clicked the MIDI authorization prompt - `controlchange-controllerXXX` to listen to a single type of control change message - and various others... - Ability to query **current note state** (currently playing or not) with [`InputChannel.getNoteState()`](/api/classes/InputChannel#getNoteState) and [`InputChannel.notesState`](/api/classes/InputChannel#notesState) array - Ability to unplug and replug a device while **retaining its state** - Better **sysex** (system exclusive) message support - **Octave transposition** can be performed at the global, input/output or channel level - and so much more! ## Backwards Compatibility Backwards compatibility was a major concern while developing the new version. While every effort has been made to ease the transition, the new version might break a few things, mostly in edge cases. Whenever it was possible, we deprecated old practices instead of completely dropping support. This means that your code should continue to work but you may see warnings in the console about the new ways to do things. :::info If you expected certain things to keep working after upgrade and they don't, please reach out in the [**Migration**](https://github.com/djipco/webmidi/discussions/categories/migration) section of the forum so whe can assess if the behaviour is expected or not and troubleshoot from there. ::: ## Architecture Change In v2, there was a top-level `WebMidi` object that provided access to a list of inputs and outputs. Most of the activity happened at the `Input` and `Output` level. This is where you would listen for inbound messages and send outbound messages. For example, if you wanted to listen for a message on a single MIDI channel, you would have specified it in the call to `addListener()` in this manner: ```javascript // In WebMidi.js version 2.5.x WebMidi.inputs[0].addListener("noteon", 7, someFunction); ``` This would listen for **noteon** events on MIDI channel 7 of input 0. **While this still works in v3**, the preferred syntax would be: ```javascript WebMidi.inputs[0].addListener("noteon", someFunction, {channels: [7]}); ``` You may think (rightly so!) that this syntax is more cumbersome. The reasoning is that, if you want to listen to events on a single channel, you should do so on the channel itself (the new [`InputChannel`](/api/classes/InputChannel) object): ```javascript WebMidi.inputs[0].channels[7].addListener("noteon", someFunction); ``` Here, `WebMidi.inputs[0].channels[7]` refers to an [`InputChannel`](/api/classes/InputChannel) object that has, for the most part, the same methods as the [`Input`](/api/classes/Input) object you were used to in v2.5.x. So, the idea is to use the [`Input`](/api/classes/Input) object if you need to listen to events on more than one channel and to use the [`InputChannel`](/api/classes/InputChannel) object to listen for events dispatched by a single channel. The exact same logic applies to the [`Output`](/api/classes/Output) and [`OutputChannel`](/api/classes/OutputChannel) objects. For example, if you want to send a **controlchange** message to all channels of an output, you can use: ```javascript WebMidi.outputs[0].sendControlChange("resonance", 123); ``` To send to multiple, but specific, channels, you can use: ```javascript WebMidi.outputs[0].sendControlChange("resonance", 123, {channels: [1, 2, 3]}); ``` To send the message to a single channel (e.g. 7), you would use: ```javascript WebMidi.outputs[0].channels[7].sendControlChange("resonance", 123); ``` In the end, the idea is to target specifically what is appropriate. Therefore, a more idiomatic snippet would be something like this: ```javascript const output = WebMidi.getOutputByName("My Awesome Midi Device"); const synth = output.channels[10]; synth.playNote("A4"); ``` Having said all that, let me reiterate that **the previous way of doing things will still work in v3**. This will give you a chance to smoothly transition to the new version. Let's recap. In v3, there is a top-level [`WebMidi`](/api/classes/WebMidi) object which has both an [`inputs`](/api/classes/WebMidi#inputs) and an [`outputs`](/api/classes/WebMidi#outputs) array. These arrays contain, respectively, a list [`Input`](/api/classes/Input) and [`Output`](/api/classes/Output) objects. The [`Input`](/api/classes/Input) and [`Output`](/api/classes/Output) objects have a `channels` array that contains a list of [`InputChannel`](/api/classes/InputChannel) or [`OutputChannel`](/api/classes/OutputChannel) objects. :::info You can also check the [CHANGELOG.md](https://github.com/djipco/webmidi/blob/master/CHANGELOG.md) file for more hints of what has changed in version 3. ::: ## Things to Watch Out For ### The `WebMidi.enable()` method now returns a promise **You can still use a callback** with [`WebMidi.enable()`](/api/classes/WebMidi#enable) and it will work just like before. However, you are now welcome to use the promise-based approach: ```javascript WebMidi.enable().then(() => { console.log("WebMidi.js has been enabled!"); }) ``` ================================================ FILE: website/docs/roadmap/_category_.json ================================================ { "label": "Roadmap", "position": 50 } ================================================ FILE: website/docs/roadmap/under-evaluation.md ================================================ --- sidebar_label: Under Evaluation sidebar_position: 2 --- # Potential Enhancements To Evaluate The analysis has not started yet. We will wait after the official launch of version 3. We do have a lot of ideas and suggestions in store. Depending on whether these features break the API or not, they may make it into version 3.x or be deployed in v4. :::info If you have suggestions, please post them for discussion to the [Feature Request](https://github.com/djipco/webmidi/discussions/categories/feature-requests) category of our GitHub Discussions. ::: ## Ideas & Suggestions to Evaluate If you feel any of these ideas should be given priority, plese explain why in the [Feature Request](https://github.com/djipco/webmidi/discussions/categories/feature-requests) category of our GitHub Discussions so I can properly triage them. * Add support for Web BLE MIDI ([browser implementation](https://github.com/skratchdot/ble-midi), [Node implementation](https://github.com/natcl/ble-midi)) * Explore compatibility with WebMidiLink. Could we create an output that points to a WebMidiLinked device? * Could we allow `WebMidi.time` to be reset? (see [discussion #213](https://github.com/djipco/webmidi/discussions/213)) * Add throttling or delay option to `sendSysex` (see discussion [#235](https://github.com/djipco/webmidi/discussions/235)). * Calculate BPM from clock messages ([Discussion #177](https://github.com/djipco/webmidi/discussions/177)) * Allow the first argument of `output.playNote( )` to be ‘0:0’ as ‘A0’, ‘7:3’ as ‘E:3’ and so on. * Add a "mute" option for inputs/outputs * Include the ability to add MIDI event listeners at the WebMidi.js level ([Issue #138](https://github.com/djipco/webmidi/issues/138)) * Emit events on `send()` so outbound MIDI messages can be listened for ([Discussion #171](https://github.com/djipco/webmidi/discussions/171)) * Add a `stopAllNotes()` method * Calculate time values and make them directly available for `songposition` and `timecode` message * Make Istanbul (nyc) break down the coverage stats by test file. * Add the ability to send grouped messages for CC events (and potentially others) * Add expliocit support for [MIDI Polyphonic Expressions](https://www.midi.org/midi-articles/midi-polyphonic-expression-mpe). * Add explicit support for [Universal System Exclusive Messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages) * This would include a `sendIdentityRequest()` method to the output object (perhaps with a `getIdentity()` companion method that waits for the response) ([Issue #117](https://github.com/djipco/webmidi/issues/117)) * This could also include the capability to query device for make/model (similar to [jzz-midi-gear](https://www.npmjs.com/package/jzz-midi-gear)) * Implement show control protocol subset * Add ability to inject Jazz-Plugin code for browsers with no native Web MIDI API support. * Add the option to create sysex plugins for various devices [forum thread](https://webmidijs.org/forum/discussion/comment/97#Comment_97) * Add [issue and PR templates](https://help.github.com/en/github/building-a-strong-community/about-issue-and-pull-request-templates) * Add continuous integration tool * Add ability to read/write MIDI files * Solid timing, midi clock, sync, transport functionality * Helper functions that help to deal with sysex checksum from specific manufacturer (Roland, checksum, etc.) * Add explicit support for Sample Dump Format (see discussion on [forum](https://webmidijs.org/forum/discussion/30/has-there-been-any-work-on-sample-dump-standard)) * Allow third-party developers to develop modules that facilitate encoding and decoding of device-specific sysex messages (see [forum discussion](https://webmidijs.org/forum/discussion/37/)) * Add timing capabilities such as syncing with Tone.js or being able to schedule events using musical notes. * Add the ability to export a MIDI file (perhaps with another lib such as [MidiWriterJS](https://www.npmjs.com/package/midi-writer-js) or [Jazz-Soft](https://jazz-soft.net/demo/WriteMidiFile.html) * SMF Support * Check if something specific needs to be done to support Electron ([this discussion](https://www.electronjs.org/docs/api/session#sessetpermissionrequesthandlerhandler)). * Evaluate whether if would be worth it to switch from the `midi` module to the `web-midi-test` module for unit tests (discussion [here](https://github.com/djipco/webmidi/discussions/223)). ## Enhancements Put On Hold For Now * Consider usage of [pipelining operator](https://github.com/tc39/proposal-pipeline-operator/blob/master/README.md#introduction) for patching webmidi function calls to a sequence * Consider using [middleware](https://github.com/unbug/js-middleware) approach for making the app pluggable * Investigate the possibility to add a `Device` object that would group inputs and outputs for a single device (see [discussion #280](https://github.com/djipco/webmidi/discussions/280) for details) * Piano roll ================================================ FILE: website/docs/roadmap/v3.md ================================================ --- sidebar_label: Version 3 sidebar_position: 1 --- # Roadmap for version 3.x ## Enhancements Still Remaining * Publish TypeScript definitions to [DefinitelyTyped](https://definitelytyped.org/guides/contributing.html) when v3 is stable (they are currently available in the `dist` directory) * Add `inputconnected`, `inputdisconnected`, `outputconnected` and `outputdisconnected` events. * There is something in `InputChannel.test.js` that prevents the tests from running correctly in `Output.test.js` * Unit tests should minimally check the 3 flavours of the library (ESM, CJS and IIFE) to make sure they each properly run. * A combined CC0 / CC32 / ProgChange method to call program changes with bank selection in a single method * The callback for `WebMidi.addListener()` and `Input.addListener()` should probably have both a `target` and a `port` property when the target is not the same as the port. Perhaps both should be kept everywhere for consistency. ## Enhancement Already Baked In * Added triggering of `portschanged` event in `WebMidi` object (v3.0.2). * Add `filter` option to allow listening to only a subset of events (e.g. specific controller change or NRPN messages, discussed in [PR #88](https://github.com/djipco/webmidi/pull/88)) * Add a way to forward inbound messages on an `Input` object to an `Output` (to behave like a physical MIDI THRU port). This could be expanded to a more elaborate filtering and routing system ([example](https://github.com/shemeshg/RtMidiWrap#routing-configuration)) * Add mechanism to Generate TypeScript type definitions (.d.ts files) * Add `getNoteState()` method to `InputChannel` so it it is possible to check if a note is currently playing or not. This allows to check for chords when a **noteon** message is received. * Properly handle when a laptop's lid is closed then reopened ([Issue #140](https://github.com/djipco/webmidi/issues/140)) * As suggested by users, allow sending MSB and LSB at once when sending control change messages ([Issue #57](https://github.com/djipco/webmidi/issues/57)). This would have to be done for CC messages 0-31 which all have a matching LSB. * Rewrite the NRPN parsing mechanism in InputChannel. I do not think it works correctly. Here are starting points: - http://tetradev.blogspot.com/2010/03/nrpns-part-2-nrpns-in-ableton-with-max.html - https://www.elektronauts.com/t/nrpn-tutorial-how-to/46669 - http://www.philrees.co.uk/nrpnq.htm * Allow `sendSysex()` to accept `Uint8Array` ([Issue #124](https://github.com/djipco/webmidi/issues/124), [forum thread](https://webmidijs.org/forum/discussion/comment/97#Comment_97)) or perhaps to accept a `Message` object that can be built from a `Uint8Array` (this needs to be carefully examined) * Add a `Message` object * Add the ability to set default values for attack velocity, release velocity, etc. ( see [forum discussion](https://webmidijs.org/forum/discussion/44/things-in-webmidi-js-2-52-that-make-me-go-huh#latest)) * Various utility methods should probably be stashed in a `Utils` class (e.g. getCcNameByNumber(), etc.) * Add convenience method to convert float and 7 bit: `to7bit()` and `toNormalized()` * Add the ability to individually transpose `Input`, `Output`, `InputChannel` and `OutputChannel`. * Add `InputChannel` and `OutputChannel` objects ([Issue #20](https://github.com/djipco/webmidi/issues/20)) * Use ES6+ modern syntax and add default export so library can be imported with `import` (Issues [#49](https://github.com/djipco/webmidi/issues/49) and [#89](https://github.com/djipco/webmidi/issues/89)) * Move to Rollup for packaging the library ([Issue #61](https://github.com/djipco/webmidi/issues/61)) * Drop support for Bower ([Issue #60](https://github.com/djipco/webmidi/issues/60)) * Extend a proper event library to allow for modern event support (probably [djipevents](https://github.com/djipco/djipevents)). * Implement port `statechange` events (`connected` and `disconnected`) * Make `WebMidi` a singleton (see example [here](https://www.sitepoint.com/javascript-design-patterns-singleton/)) * WebMidi should dispatch 'enabled' and 'disabled' event * Check that disable() really does disable everything * Add methods for channel mode messages * Implement `clear()` method ([Issue #52](https://github.com/djipco/webmidi/issues/52)) [this will automatically work when browsers add support for it but will show a warning in the console until then). * Added the new ([software](https://webaudio.github.io/web-midi-api/#dom-midioptions)) parameter for `requestMIDIAccess` [this will automatically work when browsers add support for it). * Emit events for all channel mode messages * Add `statusByte` and `dataBytes` properties to the event triggered when receiving messages ([#109](https://github.com/djipco/webmidi/issues/109)) * Deprecate the ability to send on all channels (default behaviour). This clogs up the MIDI stream and I do not really see a good use case for it. * Create a `Note` object with `duration` and `velocity` property * Add official support for Node.js ([Issue #15](https://github.com/djipco/webmidi/issues/15)) * Allow specifying different note durations and velocities in `playNote()` when using arrays ([Issue #75](https://github.com/djipco/webmidi/issues/75) and [#90](https://github.com/djipco/webmidi/issues/90)). [this is now possible with `Note` objects]. * Add [editorconfig file](https://atom.io/packages/editorconfig) * Complete test suite for all objects * Add `sendRaw()` method accepting either list of integers or `Uint8Array`. * Allow `send()` to accept `Uint8Array` ================================================ FILE: website/docs/roadmap/v4.md ================================================ --- sidebar_label: Version 4 sidebar_position: 2 --- # Features Planned for Version 4 The full analysis has not started yet. We do have a lot of [ideas](/docs/roadmap/under-evaluation) in store. Depending on whether these features break the API or not, they may make it into version 3.x or be deployed in v4. :::info If you have suggestions, please post them for discussion to the [Feature Request](https://github.com/djipco/webmidi/discussions/categories/feature-requests) category of our GitHub Discussions. ::: ================================================ FILE: website/docusaurus.config.js ================================================ const lightCodeTheme = require("prism-react-renderer/themes/github"); const darkCodeTheme = require("prism-react-renderer/themes/dracula"); const BASE_URL = "/"; /** @type {import("@docusaurus/types").DocusaurusConfig} */ module.exports = { title: "WEBMIDI.js", tagline: "Kickstart your JavaScript MIDI projects!", url: "https://webmidijs.org", baseUrl: BASE_URL, onBrokenLinks: "warn", onBrokenMarkdownLinks: "warn", favicon: "img/favicon.ico", organizationName: "djipco", projectName: "webmidi", // trailingSlash: false, scripts: [ ], themeConfig: { navbar: { // title: "My Site", logo: { alt: "WEBMIDI.js", src: "img/webmidijs-logo-dark.svg", srcDark: "img/webmidijs-logo-light.svg", }, items: [ { label: "Docs", type: "doc", docId: "index", position: "left", }, { type: "dropdown", label: "API", position: "left", items: [ { label: "3.x", to: "api/" }, { label: "2.5.3", href: "https://webmidijs.org/archives/api/v2/", }, { label: "1.0.0-beta.15", href: "http://webmidijs.org/archives/api/v1/classes/WebMidi.html" } ] }, { type: "dropdown", label: "Community", position: "left", items: [ { label: "Sponsors", to: "sponsors" }, { label: "Showcase", to: "showcase" }, { label: "GitHub Discussions", href: "https://github.com/djipco/webmidi/discussions", className: "external" }, { label: "Newsletter Subscription", href: "https://mailchi.mp/eeffe50651bd/webmidijs-newsletter", className: "external" }, { label: "Forum (Archived)", href: "https://webmidijs.org/forum/", className: "external" } ] }, { type: "dropdown", label: "Behind the Scenes", position: "left", items: [ { to: "about", label: "About", }, { to: "blog", label: "Blog", }, { to: "research", label: "Academic Research", }, ] }, { href: "https://github.com/djipco/webmidi", position: "right", className: "header-github-link", "aria-label": "GitHub Repo" }, { href: "https://twitter.com/webmidijs", position: "right", className: "header-twitter-link", "aria-label": "Twitter Feed" }, ] }, footer: { style: "dark", logo: { alt: "WebMidi.js", src: "img/webmidijs-logo-dark.svg", srcDark: "img/webmidijs-logo-light.svg", }, links: [ { title: "Docs", items: [ { label: "Quick Start", to: "/docs", }, { label: "Getting Started", to: "/docs/getting-started", }, { label: "Migration", to: "/docs/migration", }, ], }, { title: "Community", items: [ { label: "Showcase", href: "/showcase", }, { label: "Twitter", href: "https://twitter.com/webmidijs", }, ], }, { title: "More", items: [ { label: "GitHub", href: "https://github.com/djipco/webmidi", }, ], }, ], copyright: `© 2015-${new Date().getFullYear()} Jean-Philippe Côté`, }, prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, }, algolia: { apiKey: "417771b74406a78671b6592f451f2453", indexName: "webmidi", appId: "KHO24V8B5T", // Optional: see doc section below contextualSearch: true, // Optional: Algolia search parameters searchParameters: {}, //... other Algolia params placeholder: "Search website..." }, image: "img/og-card.png", metadata: [{ name: "robots", content: "max-image-preview:large" }], announcementBar: { id: "sponsor-banner", content: "" + "Sponsor ❤️ WEBMIDI.js on GitHub!" } }, presets: [ [ "@docusaurus/preset-classic", { theme: { customCss: [ require.resolve("./src/css/custom.scss"), require.resolve("./src/css/index.scss"), ], }, docs: { path: "docs", lastVersion: "current", onlyIncludeVersions: ["current"], sidebarPath: require.resolve("./sidebars.js"), editUrl: "https://github.com/djipco/webmidi/edit/master/website/", }, blog: { path: "blog", blogTitle: "Blog de Docusaurus !", blogDescription: "Un blog alimenté par Docusaurus !", postsPerPage: "ALL", }, pages: {}, gtag: { // trackingID: "UA-162785934-1", trackingID: "G-Z65JF8XMJG", }, googleAnalytics: { // trackingID: "UA-162785934-1", trackingID: "G-Z65JF8XMJG", } } ], ], plugins: [ [ "@docusaurus/plugin-content-docs", { id: "api", path: "api", routeBasePath: "api", sidebarPath: require.resolve("./sidebars.js"), }, ], [ "docusaurus-plugin-sass", {} ], [ "@docusaurus/plugin-client-redirects", { redirects: [ { from: ["/latest/classes/WebMidi.html"], // string | string[] to: "/api/", }, ], }, ], ], }; ================================================ FILE: website/package.json ================================================ { "name": "docusaurus", "version": "0.0.0", "private": true, "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { "@docusaurus/core": "^3.0.0", "@docusaurus/plugin-client-redirects": "^3.0.0", "@docusaurus/plugin-content-blog": "^3.0.0", "@docusaurus/plugin-content-docs": "^3.0.0", "@docusaurus/preset-classic": "^3.0.0", "@docusaurus/theme-search-algolia": "^3.0.0", "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^6.3.1", "clsx": "^1.1.1", "docusaurus-plugin-sass": "^0.2.5", "file-loader": "^6.2.0", "prism-react-renderer": "^1.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-helmet": "^6.1.0", "sass": "^1.43.4", "typescript": "^4.4.2", "url-loader": "^4.1.1" }, "browserslist": { "production": [ ">0.5%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: website/sidebars.js ================================================ /** * Creating a sidebar enables you to: - create an ordered group of docs - render a sidebar for each doc of that group - provide next/previous navigation The sidebars can be generated from the filesystem, or explicitly defined here. Create as many sidebars as you want. */ module.exports = { // By default, Docusaurus generates a sidebar from the docs folder structure tutorialSidebar: [{type: "autogenerated", dirName: "."}], // But you can create a sidebar manually /* tutorialSidebar: [ { type: 'category', label: 'Tutorial', items: ['hello'], }, ], */ }; ================================================ FILE: website/src/components/Button.js ================================================ import React from "react"; import styles from "./Button.module.scss"; /*export default function Button({children, type, href, target}) { return ( ); }*/ export default function Button(props) { const component = "Button"; const{ children, href, type, target, } = props; return (
{children}
); } ================================================ FILE: website/src/components/Button.module.css ================================================ .Button { background-color: var(--color-accent); width: fit-content; height: fit-content; border-radius: 10px; position: relative; overflow: hidden; text-align: center; } .Button a { display: block; font: bold 1.56rem var(--font-secondary), sans-serif; padding: var(--spacing-sm) 1.5em; color: var(--color-text-primary); text-decoration: none; position: relative; z-index: 2; } .Button .buttonBg { width: 100%; height: 100%; position: absolute; } .Button.button-bg-full { border: solid 4px var(--color-accent); } .Button.button-bg-full .buttonBg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-full:hover { border: solid 4px var(--color-accent-lighter); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-full:hover .buttonBg { left: 0; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-empty { background-color: rgba(0, 0, 0, 0); border: solid 4px var(--color-accent); } .Button.button-bg-empty a { color: var(--color-accent); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-empty .buttonBg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-empty:hover { border: solid 4px var(--color-accent-lighter); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-empty:hover a { color: var(--color-white); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .Button.button-bg-empty:hover .buttonBg { left: 0; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } /*# sourceMappingURL=Button.module.css.map */ ================================================ FILE: website/src/components/Button.module.scss ================================================ $ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); .Button { background-color: var(--color-accent); width: fit-content; height: fit-content; border-radius: 10px; position: relative; overflow: hidden; text-align: center; a { display: block; font: bold 1.56rem var(--font-secondary), sans-serif; padding: var(--spacing-sm) 1.5em; color: var(--color-text-primary); text-decoration: none; position: relative; z-index: 2; } .buttonBg { width: 100%; height: 100%; position: absolute; } &.button-bg-full { border: solid 4px var(--color-accent); .buttonBg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s $ease-in-out-circ; } &:hover { border: solid 4px var(--color-accent-lighter); transition: all 0.3s $ease-in-out-circ; .buttonBg { left: 0; transition: all 0.3s $ease-in-out-circ; } } } &.button-bg-empty { background-color: rgba(0, 0, 0, 0%); border: solid 4px var(--color-accent); a { color: var(--color-accent); transition: all 0.3s $ease-in-out-circ; } .buttonBg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s $ease-in-out-circ; } &:hover { border: solid 4px var(--color-accent-lighter); transition: all 0.3s $ease-in-out-circ; a { color: var(--color-white); transition: all 0.3s $ease-in-out-circ; } .buttonBg { left: 0; transition: all 0.3s $ease-in-out-circ; } } } } ================================================ FILE: website/src/components/Column.js ================================================ import React from "react"; import styles from "./Column.module.scss"; export default function Column({children, type,}) { const component = "Column"; return (
{children}
); } ================================================ FILE: website/src/components/Column.module.css ================================================ .Column { display: grid; } .col-2 { grid-template-columns: 48% 48%; justify-content: space-between; align-items: center; } @media screen and (max-width: 1000px) { .col-2 { display: grid; grid-template-columns: 1fr; text-align: center; } } /*# sourceMappingURL=Column.module.css.map */ ================================================ FILE: website/src/components/Column.module.scss ================================================ .Column{ display: grid; } .col-2 { grid-template-columns: 48% 48%; justify-content: space-between; align-items: center; @media screen and(max-width: 1000px){ display: grid; grid-template-columns: 1fr; text-align: center; } } ================================================ FILE: website/src/components/HomepageFeatures.js ================================================ import React from "react"; import clsx from "clsx"; import styles from "./HomepageFeatures.module.css"; const FeatureList = [ // { // title: 'Easy to Use', // Svg: require('../../static/img/undraw_docusaurus_mountain.svg').default, // description: ( // <> // Docusaurus was designed from the ground up to be easily installed and // used to get your website up and running quickly. // // ), // }, // { // title: 'Focus on What Matters', // Svg: require('../../static/img/undraw_docusaurus_tree.svg').default, // description: ( // <> // Docusaurus lets you focus on your docs, and we'll do the chores. Go // ahead and move your docs into the docs directory. // // ), // }, // { // title: 'Powered by React', // Svg: require('../../static/img/undraw_docusaurus_react.svg').default, // description: ( // <> // Extend or customize your website layout by reusing React. Docusaurus can // be extended while reusing the same header and footer. // // ), // }, ]; function Feature({Svg, title, description}) { return (

{title}

{description}

); } export default function HomepageFeatures() { return (
{FeatureList.map((props, idx) => ( ))}
); } ================================================ FILE: website/src/components/HomepageFeatures.module.css ================================================ /* stylelint-disable docusaurus/copyright-header */ .features { display: flex; align-items: center; padding: 2rem 0; width: 100%; } .featureSvg { height: 200px; width: 200px; } ================================================ FILE: website/src/components/InformationBar.js ================================================ import React from "react"; import styles from "./InformationBar.module.scss"; export default function InformationBar({children, type,}) { const component = "InformationBar"; return (

{children}

); } ================================================ FILE: website/src/components/InformationBar.module.css ================================================ .InformationBar { background-color: var(--color-bg-secondary); padding: var(--spacing-xl) 0; color: var(--color-white); text-align: center; } .InformationBar p { font-size: 1.87rem; line-height: 120%; margin: 0; } /*# sourceMappingURL=InformationBar.module.css.map */ ================================================ FILE: website/src/components/InformationBar.module.scss ================================================ .InformationBar{ background-color: var(--color-bg-secondary); padding: var(--spacing-xl) 0; color: var(--color-white); text-align: center; p { font-size: 1.87rem; line-height: 120%; margin:0; } } ================================================ FILE: website/src/css/custom.css ================================================ /* stylelint-disable docusaurus/copyright-header */ @import url("https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,400;0,900;1,900&family=Lato:wght@400;700&display=swap"); :root { --ifm-color-primary: #f9d137; --ifm-color-primary-dark: #f8ca19; --ifm-color-primary-darker: #f8c70b; --ifm-color-primary-darkest: #cfa506; --ifm-color-primary-light: #fad855; --ifm-color-primary-lighter: #fadb63; --ifm-color-primary-lightest: #fce590; --ifm-code-font-size: 95%; --ifm-link-color: #759DCE; --ifm-code-padding-horizontal: 0.2rem; --color-white: #ffffff; --color-black: #1c1e21; --color-text-primary: var(--color-black); --color-text-secondary: var(--color-white); --color-text-tertiary: #999; --color-bg-primary: var(--color-white); --color-bg-secondary: var(--color-black); --color-bg-tertiary: #F8F8F8; --color-accent: #ffd000; --color-accent-lighter: #ffe571; --color-accent-darker: #e0ba0f; --spacing-xs: 15px; --spacing-sm: 20px; --spacing-md: 30px; --spacing-lg: 50px; --spacing-xl: 100px; --spacing-xxl: 175px; --spacing-xxxl: 250px; --font-primary: "Lato", sans-serif; --font-secondary: "Exo", sans-serif; --font-size-content-desktop: 1.125rem; --font-size-content-mobile: 1rem; --font-size-button: 1.25rem; --font-size-h1-desktop: 3rem; --font-size-h1-mobile: 2.5rem; --font-size-h2-desktop: 1.8rem; --font-size-h2-mobile: 1.6rem; --font-size-h3-desktop: 1.2rem; --font-size-h3-mobile: 1.125rem; } @media screen and (max-width: 500px) { :root { --font-size-h1-desktop: 3.5rem; } } html[data-theme=dark]:root { --color-text-primary: var(--color-white); --color-text-secondary: var(--color-black); --color-bg-primary: var(--color-black); --color-bg-secondary: #222428; --color-bg-tertiary: #222428; } .docusaurus-highlight-code-line { background-color: rgba(0, 0, 0, 0.1); display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); } .navbar__brand { height: 3rem; } .navbar__brand .navbar__logo { height: 3.5rem; } .docs-wrapper main .container article .markdown header:nth-child(2) { display: none; } main article a:link { font-weight: var(--ifm-font-weight-bold); } html[data-theme=dark] .docusaurus-highlight-code-line { background-color: rgba(0, 0, 0, 0.3); } .header-github-link { padding: 4px; } .header-github-link:hover { opacity: 0.6; } .header-github-link:before { content: ""; width: 24px; height: 24px; display: flex; background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; } html[data-theme=dark] .header-github-link:before { filter: invert(1); } .header-twitter-link { margin-right: 0.5em; padding: 4px; } .header-twitter-link:hover { opacity: 0.6; } .header-twitter-link:before { content: ""; width: 25px; height: 25px; display: flex; background: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.0' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 24 24' enable-background='new 0 0 24 24' xml:space='preserve'%3E%3Cpath fill='%23191717' d='M12,0C5.4,0,0,5.4,0,12s5.4,12,12,12s12-5.4,12-12S18.6,0,12,0z M17.4,9.3c0,0.1,0,0.2,0,0.4 c0,3.6-2.8,7.8-7.8,7.8c-1.5,0-3-0.4-4.2-1.2c0.2,0,0.4,0,0.7,0c1.2,0,2.4-0.4,3.4-1.2c-1.2,0-2.2-0.8-2.6-1.9 c0.4,0.1,0.8,0.1,1.2,0c-1.3-0.3-2.2-1.4-2.2-2.7v0c0.4,0.2,0.8,0.3,1.3,0.3C5.9,9.9,5.5,8.3,6.2,7.1c1.4,1.7,3.5,2.8,5.7,2.9 c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.5,0.9-2c1.1-1,2.9-1,3.9,0.1c0.6-0.1,1.2-0.3,1.8-0.7c-0.2,0.6-0.6,1.2-1.2,1.5 c0.5-0.1,1.1-0.2,1.6-0.4C18.4,8.4,17.9,8.9,17.4,9.3z'/%3E%3C/svg%3E%0A") no-repeat; } html[data-theme=dark] .header-twitter-link:before { filter: invert(1); } .docs-wrapper article .markdown header:nth-of-type(2) { display: none; } div[role=banner] { background-color: var(--ifm-color-primary); } li a.dropdown__link svg { display: none; } li a.recommended { font-weight: bold; } li a.dropdown__link.external svg { display: inline; } .parameter-table-container table { display: table; width: 100%; } .parameter-table-container table th:first-of-type { width: 18%; } .parameter-table-container table th:nth-of-type(2) { width: 18%; } .parameter-table-container table th:nth-of-type(3) { width: 18%; } .parameter-table-container table th:nth-of-type(4) { width: 46%; } /* ========================================================================== HEADINGS / ELEMENT ========================================================================== */ h1 { font-family: var(--font-secondary); font-weight: 900; font-size: var(--font-size-h1-mobile); } @media screen and (min-width: 1024px) { h1 { font-size: var(--font-size-h1-desktop); } } h2 { font-family: var(--font-secondary); font-weight: 700; font-size: var(--font-size-h2-mobile); margin-top: 2.4rem; } @media screen and (min-width: 1024px) { h2 { font-size: var(--font-size-h2-desktop); } } h3 { font-family: var(--font-secondary); font-weight: 700; font-size: var(--font-size-h3-mobile); } @media screen and (min-width: 1024px) { h3 { font-size: var(--font-size-h3-desktop); } } html[data-theme=light] a.cem-logo img { filter: invert(1); } a.user-icon img { border-radius: 50px; border: 1px solid black; margin: 0.5rem; } /*# sourceMappingURL=custom.css.map */ ================================================ FILE: website/src/css/custom.scss ================================================ /* stylelint-disable docusaurus/copyright-header */ // Global custom stylesheet for Docusaurus. The 'classic' template bundles Infima. Infima is a CSS // framework designed for content-centric websites. // Import @import url("https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,400;0,900;1,900&family=Lato:wght@400;700&display=swap"); // Default Infima variables //////////////////////////////////////////////////////////////////////// :root { --ifm-color-primary: #f9d137; --ifm-color-primary-dark: #f8ca19; --ifm-color-primary-darker: #f8c70b; --ifm-color-primary-darkest: #cfa506; --ifm-color-primary-light: #fad855; --ifm-color-primary-lighter: #fadb63; --ifm-color-primary-lightest: #fce590; --ifm-code-font-size: 95%; --ifm-link-color: #759DCE; //--ifm-background-color: #eee; --ifm-code-padding-horizontal: 0.2rem; // Base color --color-white: #ffffff; --color-black: #1c1e21; // Project Color --color-text-primary: var(--color-black); --color-text-secondary: var(--color-white); --color-text-tertiary: #999; --color-bg-primary: var(--color-white); --color-bg-secondary: var(--color-black); --color-bg-tertiary: #F8F8F8; --color-accent: #ffd000; --color-accent-lighter: #ffe571; --color-accent-darker: #e0ba0f; // Dimensions --spacing-xs: 15px; --spacing-sm: 20px; --spacing-md: 30px; --spacing-lg: 50px; --spacing-xl: 100px; --spacing-xxl: 175px; --spacing-xxxl: 250px; --font-primary: "Lato", sans-serif; --font-secondary: "Exo", sans-serif; --font-size-content-desktop: 1.125rem; --font-size-content-mobile: 1rem; --font-size-button: 1.25rem; /// Grosseur des textes --font-size-h1-desktop: 3rem; --font-size-h1-mobile: 2.5rem; --font-size-h2-desktop: 1.8rem; --font-size-h2-mobile: 1.6rem; --font-size-h3-desktop: 1.2rem; --font-size-h3-mobile: 1.125rem; @media screen and(max-width: 500px){ --font-size-h1-desktop: 3.5rem; } } html[data-theme='dark']:root{ --color-text-primary: var(--color-white); --color-text-secondary: var(--color-black); --color-bg-primary: var(--color-black); --color-bg-secondary: #222428; --color-bg-tertiary: #222428; } $breakpoint-xs: 375px; $breakpoint-sm: 768px; $breakpoint-md: 1024px; $breakpoint-lg: 1440px; $breakpoint-xl: 1920px; .docusaurus-highlight-code-line { background-color: rgba(0, 0, 0, 0.1); display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); } .navbar__brand { height: 3rem; .navbar__logo { height: 3.5rem; } } .docs-wrapper main .container article .markdown header:nth-child(2) { display: none; } main article a:link { font-weight: var(--ifm-font-weight-bold); } html[data-theme='dark'] .docusaurus-highlight-code-line { background-color: rgba(0, 0, 0, 0.3); } .header-github-link { //margin-right: 0.5em; padding: 4px; } .header-github-link:hover { opacity: 0.6; } .header-github-link:before { content: ''; width: 24px; height: 24px; display: flex; background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; } html[data-theme=dark] .header-github-link:before { filter: invert(1); } .header-twitter-link { margin-right: 0.5em; padding: 4px; } .header-twitter-link:hover { opacity: 0.6; } .header-twitter-link:before { content: ''; width: 25px; height: 25px; display: flex; background: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.0' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 24 24' enable-background='new 0 0 24 24' xml:space='preserve'%3E%3Cpath fill='%23191717' d='M12,0C5.4,0,0,5.4,0,12s5.4,12,12,12s12-5.4,12-12S18.6,0,12,0z M17.4,9.3c0,0.1,0,0.2,0,0.4 c0,3.6-2.8,7.8-7.8,7.8c-1.5,0-3-0.4-4.2-1.2c0.2,0,0.4,0,0.7,0c1.2,0,2.4-0.4,3.4-1.2c-1.2,0-2.2-0.8-2.6-1.9 c0.4,0.1,0.8,0.1,1.2,0c-1.3-0.3-2.2-1.4-2.2-2.7v0c0.4,0.2,0.8,0.3,1.3,0.3C5.9,9.9,5.5,8.3,6.2,7.1c1.4,1.7,3.5,2.8,5.7,2.9 c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.5,0.9-2c1.1-1,2.9-1,3.9,0.1c0.6-0.1,1.2-0.3,1.8-0.7c-0.2,0.6-0.6,1.2-1.2,1.5 c0.5-0.1,1.1-0.2,1.6-0.4C18.4,8.4,17.9,8.9,17.4,9.3z'/%3E%3C/svg%3E%0A") no-repeat; } html[data-theme=dark] .header-twitter-link:before { filter: invert(1); } // Styles applicable to "docs" and "api" /////////////////////////////////////////////////////////// .docs-wrapper article .markdown header:nth-of-type(2) { display: none; } div[role=banner] { background-color: var(--ifm-color-primary); } // Styles applicable to menu /////////////////////////////////////////////////////////////////////// li a.dropdown__link svg { display: none; } li a.recommended { font-weight: bold; } li a.dropdown__link.external svg { display: inline; } // Styles applicable to parameter tables in API //////////////////////////////////////////////////// .parameter-table-container { table { display: table; width: 100%; th:first-of-type { width: 18%; } th:nth-of-type(2) { width: 18%; } th:nth-of-type(3) { width: 18%; } th:nth-of-type(4) { width: 46%; } } } /* ========================================================================== HEADINGS / ELEMENT ========================================================================== */ h1 { font-family: var(--font-secondary); font-weight: 900; font-size: var(--font-size-h1-mobile); @media screen and (min-width: $breakpoint-md) { font-size: var(--font-size-h1-desktop); } } h2 { font-family: var(--font-secondary); font-weight: 700; font-size: var(--font-size-h2-mobile); margin-top: 2.4rem; @media screen and (min-width: $breakpoint-md) { font-size: var(--font-size-h2-desktop); } } h3 { font-family: var(--font-secondary); font-weight: 700; font-size: var(--font-size-h3-mobile); @media screen and (min-width: $breakpoint-md) { font-size: var(--font-size-h3-desktop); } } html[data-theme=light] a.cem-logo img { filter: invert(1); } a.user-icon img { border-radius: 50px; border: 1px solid black; margin: 0.5rem; } ================================================ FILE: website/src/css/index.css ================================================ .hero { padding: var(--spacing-lg) 0; min-height: 60vh; color: var(--color-text-primary); display: flex; align-items: center; background: var(--color-bg-primary); text-align: center; } @media screen and (max-width: 1000px) { .hero { min-height: 40vh; padding-top: 0; } } .hero .logo { max-width: 600px; height: 300px; background: center url("/img/webmidijs-logo-dark.svg") no-repeat; margin: auto; } @media screen and (max-width: 500px) { .hero .logo { height: 10em; margin: var(--spacing-md) 0; } } html[data-theme=dark] .hero .logo { background: center url("/img/webmidijs-logo-light.svg") no-repeat; } .hero span { font-family: var(--font-secondary); font-size: 2rem; display: block; margin-bottom: var(--spacing-sm); font-weight: bold; color: var(--color-accent); } .hero .cta { display: flex; flex-wrap: wrap; justify-content: center; /*@media screen and(max-width: 1000px){ justify-content: center; }*/ } .hero .cta .Button { margin: var(--spacing-sm) 0 0 var(--spacing-sm); } .hero .cta .Button:nth-child(1) { margin-left: 0; } .hero .img { margin: auto; } @media screen and (max-width: 1000px) { .hero .texts { order: 2; } } /*======== Presentation ========*/ .presentation { margin: var(--spacing-lg) 0; } .presentation h2 { text-align: center; } .presentation p { font-size: 1.5rem; margin: var(--spacing-sm) 0; } .presentation .media { background-color: var(--color-bg-tertiary); border-radius: 20px; width: 100%; height: 375px; display: flex; justify-content: center; align-items: center; margin: var(--spacing-xl) auto; box-shadow: 10px 10px 0 rgba(255, 208, 0, 0.5); } @media screen and (max-width: 1000px) { .presentation .media { width: 90%; margin: var(--spacing-md) auto; } } .presentation .media .imgMedia { width: 80%; height: 80%; } html[data-theme=light] .presentation .media .imgMedia { filter: invert(1); } .presentation .media :nth-child(1) { background: center url("/img/front-page/webmidi-demonstration.svg") no-repeat; } @media screen and (max-width: 1000px) { .presentation .media :nth-child(1) { background: center url("/img/front-page/webmidi-demonstration-vertical.svg") no-repeat; } } /*# sourceMappingURL=index.css.map */ ================================================ FILE: website/src/css/index.scss ================================================ .hero{ padding: var(--spacing-lg) 0; min-height: 60vh; color: var(--color-text-primary); display: flex; align-items: center; background: var(--color-bg-primary); text-align: center; @media screen and(max-width: 1000px){ min-height: 40vh; padding-top: 0; } .logo { max-width: 600px; height: 300px; background: center url("/img/webmidijs-logo-dark.svg") no-repeat; margin: auto; @media screen and(max-width: 500px){ height: 10em; margin: var(--spacing-md) 0; } html[data-theme=dark] &{ background: center url("/img/webmidijs-logo-light.svg") no-repeat; } } span { font-family: var(--font-secondary); font-size: 2rem; display: block; margin-bottom: var(--spacing-sm); font-weight: bold; color: var(--color-accent); } .cta { display: flex; flex-wrap: wrap; justify-content: center; .Button{ margin: var(--spacing-sm) 0 0 var(--spacing-sm); } .Button:nth-child(1){ margin-left: 0; } /*@media screen and(max-width: 1000px){ justify-content: center; }*/ } .img{ margin: auto; } .texts{ @media screen and(max-width: 1000px){ order: 2; } } } /*======== Presentation ========*/ .presentation { margin: var(--spacing-lg) 0; h2 { text-align: center; } p{ font-size: 1.5rem; margin: var(--spacing-sm) 0; } .media { background-color: var(--color-bg-tertiary); border-radius: 20px; width: 100%; height: 375px; display: flex; justify-content: center; align-items: center; margin: var(--spacing-xl) auto; box-shadow: 10px 10px 0 rgba(255, 208, 0, 0.5); @media screen and(max-width: 1000px){ width: 90%; margin: var(--spacing-md) auto; } .imgMedia{ width: 80%; height: 80%; html[data-theme=light] &{ filter: invert(1); } } } .media :nth-child(1){ background: center url("/img/front-page/webmidi-demonstration.svg") no-repeat; @media screen and(max-width: 1000px){ background: center url("/img/front-page/webmidi-demonstration-vertical.svg") no-repeat; } } } ================================================ FILE: website/src/pages/about/index.md ================================================ # About ## Who created this? **WEBMIDI.js** is a passion project of mine. I am Jean-Philippe Côté (a.k.a. [djip.co](https://djip.co)), an [academic](https://www.cegepmontpetit.ca/cegep/recherche/professeurs-chercheurs/jean-philippe-cote) and artist with particular interests in creative coding, interactive arts and music technology. You can reach out to me in different ways: * Twitter: **[@djipco](https://twitter.com/djipco)** or **[@webmidijs](https://twitter.com/webmidijs)** * Website: **[https://djip.co/](https://djip.co)** * GitHub: **[https://github.com/djipco/](https://github.com/djipco)** One of my students, **Jean-Marie Gariépy** has also been helping out in various capacities with the creation of this website. Let's all thank him for his contribution! 👏 ## Sponsoring the project You can [sponsor the project](https://github.com/sponsors/djipco/) by becoming a GitHub sponsor. All you need is a GitHub account. If you see value in this library, please consider a contribution. 🙏🏻 ## Licensing Starting with version 3.0.0, this library is licensed under the **Apache License, Version 2.0**. You may not use this library except in compliance with this license. You may obtain a copy at: * [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under this license is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the above license for the specific language governing permissions and limitations. © 2015-2023, Jean-Philippe Côté. ================================================ FILE: website/src/pages/index.js ================================================ import React from "react"; import useBaseUrl from '@docusaurus/useBaseUrl'; import Layout from "@theme/Layout"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Button from "../components/Button"; import Column from "../components/Column"; import InformationBar from "../components/InformationBar"; function HomepageHero() { const {siteConfig} = useDocusaurusContext(); return (
{siteConfig.tagline}
); } function Presentation() { const {siteConfig} = useDocusaurusContext(); return (

What is {siteConfig.title}?

The existing Web MIDI API is a really exciting addition to the web platform allowing a web page to interact with MIDI musical instruments. However, while great, most developers will find the original API to be too low-level for their needs. Having to perform binary arithmetic or needing to read the 300-page MIDI spec is no fun (trust us on this!). The goal behind WEBMIDI.js is to get you started with your web-based MIDI project as efficiently as possible.

); } export default function Home() { const {siteConfig} = useDocusaurusContext(); return (
Version 3.0 has been released!
Subscribe to the newsletter
to learn about all the new features.
); } ================================================ FILE: website/src/pages/index.module.css ================================================ /* stylelint-disable docusaurus/copyright-header */ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ /* .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; } @media screen and (max-width: 966px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } */ .hero { padding: var(--spacing-lg); min-height: 60vh; display: flex; align-items: center; } .hero h1 { margin: 0; text-transform: uppercase; } .hero span { font-family: var(--font-secondary); font-size: 1.56rem; display: block; margin-bottom: var(--spacing-sm); } .hero .cta { display: flex; } .hero .cta .button:nth-child(n) { margin-left: var(--spacing-md); } .hero .cta .button:first-child { margin-left: 0; } /*======== Boutons ========*/ .button { background-color: var(--color-accent); width: fit-content; border-radius: 10px; position: relative; overflow: hidden; } .button a { display: block; padding: var(--spacing-sm) var(--spacing-lg); font-weight: bold; font-family: var(--font-secondary); font-size: 1.56rem; position: relative; z-index: 2; } .button .button--bg { width: 100%; height: 100%; position: absolute; } .button.button-bg-full .button--bg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .button.button-bg-full:hover .button--bg { left: 0; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .button.button-bg-empty { background-color: rgba(0, 0, 0, 0); border: solid 4px var(--color-accent); } .button.button-bg-empty a { color: var(--color-accent); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .button.button-bg-empty .button--bg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .button.button-bg-empty:hover a { color: var(--color-white); transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } .button.button-bg-empty:hover .button--bg { left: 0; transition: all 0.3s cubic-bezier(0.785, 0.135, 0.15, 0.86); } /*======== Columns ========*/ .col-2 { display: grid; grid-template-columns: 48% 48%; justify-content: space-between; align-items: center; } /*# sourceMappingURL=index.module.css.map */ ================================================ FILE: website/src/pages/index.module.scss ================================================ /* stylelint-disable docusaurus/copyright-header */ /** * CSS files with the .module.css suffix will be treated as CSS modules * and scoped locally. */ /* .heroBanner { padding: 4rem 0; text-align: center; position: relative; overflow: hidden; } @media screen and (max-width: 966px) { .heroBanner { padding: 2rem; } } .buttons { display: flex; align-items: center; justify-content: center; } */ .hero{ padding: var(--spacing-lg); min-height: 60vh; display: flex; align-items: center; h1 { margin: 0; text-transform: uppercase; } span { font-family: var(--font-secondary); font-size: 1.56rem; display: block; margin-bottom: var(--spacing-sm); } .cta { display: flex; .button:nth-child(n) { margin-left: var(--spacing-md); } .button:first-child { margin-left: 0; } } } /*======== Boutons ========*/ $ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); .button { background-color: var(--color-accent); width: fit-content; border-radius: 10px; position: relative; overflow: hidden; a { display: block; padding: var(--spacing-sm) var(--spacing-lg); font-weight: bold; font-family: var(--font-secondary); font-size: 1.56rem; position: relative; z-index: 2; } .button--bg { width: 100%; height: 100%; position: absolute; } &.button-bg-full { .button--bg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s $ease-in-out-circ; } &:hover { .button--bg { left: 0; transition: all 0.3s $ease-in-out-circ; } } } &.button-bg-empty { background-color: rgba(0, 0, 0, 0%); border: solid 4px var(--color-accent); a { color: var(--color-accent); transition: all 0.3s $ease-in-out-circ; } .button--bg { background-color: var(--color-accent-lighter); z-index: 1; top: 0; left: -100%; transition: all 0.3s $ease-in-out-circ; } &:hover { a { color: var(--color-white); transition: all 0.3s $ease-in-out-circ; } .button--bg { left: 0; transition: all 0.3s $ease-in-out-circ; } } } } /*======== Columns ========*/ .col-2 { display: grid; grid-template-columns: 48% 48%; justify-content: space-between; align-items: center; } ================================================ FILE: website/src/pages/research/index.md ================================================ # Academic Research I invite all academics and researchers to show their support for this project by properly citing it wherever appropriate in your publications and references. ## Citing this Software I wrote a [paper about WEBMIDI.js](https://nime.pubpub.org/pub/user-friendly-midi-in-the-web-browser) and, more specifically, about how it tries to address the usability shortcomings of the Web MIDI API. I invite academics to cite it in their publication whenever appropriate: > Côté, J.-P. (2022). User-Friendly MIDI in the Web Browser. NIME 2022. > https://doi.org/10.21428/92fbeb44.388e4764 You can also cite the library itself like so (APA style): > Côté, J. P. (2025). WEBMIDI.js v3.1.14 [Computer Software]. Retrieved from > https://github.com/djipco/webmidi ## Papers Citing Usage of WEBMIDI.js * Arora, A., Tandori, E., Marshall, J. et Favilla, S. (2025). **Designing Sensory NIME for Autism** (p. 436‑442). Proceedings of the International Conference on New Interfaces for Musical Expression. https://www.nime.org/proc/nime2025_63/index.html * Leischner, V., & Husa, P. (2023). Sonification of a Juggling Performance Using Spatial Audio. Proceedings of the ACM on Computer Graphics and Interactive Techniques, 6(2), 1–6. https://doi.org/10.1145/3597619 * Baratè, A., & Ludovico, L. A. (2022). **Web MIDI API: State of the Art and Future Perspectives**. Journal of the Audio Engineering Society, 70(11), 918–925. https://www.aes.org/e-lib/browse.cfm?elib=22016 * Graber, Z., & Henrion, W. (2021). **Music For Me**. Computer Science and Engineering Senior Theses. https://scholarcommons.scu.edu/cseng_senior/203 * Kostek, B. (Éd.). (2021). **Postępy badań w inżynierii dźwięku i obrazu**. Politechnika Wrocławska, Oficyna Wydawnicza. https://doi.org/10.37190/ido2021 * Walczak, M., & Łukasik, E. (2021). **Rozproszony system generowania, edycji i transmisji dźwięku wykorzystujący interfejsy Web Audio API, WebRTC i Web MIDI API**. In Postępy badań w inżynierii dźwięku i obrazu: Nowe trendy i zastosowania technologii dźwięku wielokanałowego oraz badania jakości dźwięku (pp. 83–104). Oficyna Wydawnicza Politechniki Wrocławskiej. https://doi.org/10.37190/ido2021 * Krawczuk, J. (2020). **Real-Time and Post-Hoc-Visualizations of Guitar Performances as a Support for Music Education**. https://doi.org/10/gkb622 * Lundh Haaland, M. (2020). **The Player as a Conductor: Utilizing an Expressive Performance System to Create an Interactive Video Game Soundtrack** (Dissertation). Retrieved from http://urn.kb.se/resolve?urn=urn:nbn:se:kth:diva-281324 * Bazin, T. & Hadjeres, G. (2019). **NONOTO: A Model-agnostic Web Interface for Interactive Music Composition by Inpainting**, presented at 10th International Conference on Computational Creativity, Charlotte, 2019. Retrieved from https://arxiv.org/abs/1907.10380 * Smith, A. (2019). **Sonification: Turning the Yield Curve into Music**. FT.com. http://search.proquest.com/docview/2191715473/fulltext/3D4C05EAFC6A4AEEPQ/1?accountid=14719 * Cárdenas, A. & Mauricio B. (2018). **Diseño y desarrollo de un prototipo para integración de Tecnología de Tracking 3d con Tecnología MIDI** [Doctoral dissertation, Pontificia Universidad Católica del Ecuador]. Retrieved from http://repositorio.puce.edu.ec/handle/22000/15838 If you are using WEBMIDI.js in your research, I would love to know about it. To notify me, you can simply [drop me a note](https://djip.co/contact). ================================================ FILE: website/src/pages/showcase/index.md ================================================ --- toc_max_heading_level: 2 --- # Showcase WebMidi.js is being used by amazing people to create awesome projects. Here are some examples of what the library can do. :::tip Contribute I would love for your project to be listed here. To add it, simply [modify](https://github.com/djipco/webmidi/edit/develop/website/src/pages/showcase/index.md) this page on GitHub and submit a pull request. ::: --- ## DAWs & Editors * ### [DAWG](https://dawg.dev/) Created by: **Jacob Smith, Amir Eldesoky, Alex ODonnell & Matt DeSilva** Digital Audio Workstation (DAW) application built using web technologies and distributed as a native application using Electron. ## Education * ### [Learn Push 2](https://github.com/greyivy/learn-push2-with-svelte) Created by: **Ivy** A website to learn chords, scales and music theory with the Push 2 controller. * ### [Raaga](https://raaga.riteshkr.com/) Created by: **Ritesh Kumar, Jack Hsu, Prateek Bhatnagar, Sruthi, Majid Hajian & Rohit Kokate** An online app to play and learn music on a keyboard. * ### [Shared Piano](https://musiclab.chromeexperiments.com/Shared-Piano/) Created by: **Yotam Mann** Shared Piano is a simple tool for remote music teaching and collaboration that lets you play music together live on the web. * ### [Chromatone](https://chromatone.center/) Created by: **Denis Starov** An interactive educational web-site for the visual music language based on associating chromatic notes with spectral colors. * ### [Chromatone](https://chromatone.center/) Created by: **Denis Starov** An interactive educational web-site for the visual music language based on associating chromatic notes with spectral colors. * ### [WebAudio Generator](https://webaudio.simmsreeve.com/) Created by: **Joe Reeve & Magnus** A user interface to facilitate the generation of linear WebAudio code. * ### [Midi Sandbox](https://midisandbox.com/) Created by: **Jon Lee** A collection of midi responsive widgets made to aid musicians, teachers, and students in their education, creative process, and practice routine. * ### [Virtual Piano Keyboard](https://muted.io/piano/) Created by: **Sébastien Noël** A simple way to practice playing a piano keyboard online with helpers like chord identification and locking to a scale. ## Experiments * ### [A.I. Duet](https://experiments.withgoogle.com/ai-duet) Created by: **Yotam Mann** This experiment lets you play a piano duet with the computer. Just play some notes, and the computer will respond to your melody. * ### [Eternal](https://github.com/kousun12/eternal) Created by: **Rob Cheung** Eternal is node-based sound and music programming and exploration environment. * ### [Mideo](https://github.com/jwktje/mideo) Created by: **Jan Willem Kilkman** A Mac application that turns videos into music. It uses color tracking in combination with a grid to generate MIDI data. * ### [Sonification: turning the yield curve into music](https://www.ft.com/content/80269930-40c3-11e9-b896-fe36ec32aece) Created by: **Alan Smith** This experiment turns the US yield curve into music and uses WEBMIDI.js to generate the MIDI note messages. * ### [Seeing Music](https://experiments.withgoogle.com/seeing-music) Created by: **Jay Alan Zimmerman, Yotam Mann, Claire Kearney-Volpe, Luisa Pereira, Kyle Phillips, and Google Creative Lab** An experiment to experience music visually. This is a tool for visualizing music. You can turn on your mic to sing or play sounds. You can also drop in your own audio or video file. * ### [webmidirtc](https://github.com/philmillman/webmidirtc) Created by: **philmillman** This project is meant to demonstrate controlling hardware synths using Web MIDI (via WebMidi.js) over a WebRTC video call (using Daily.) ## Music Hardware Control * ### [Mercury7 editor](https://github.com/francoisgeorgy/mercury7-web-editor) Created by: **François Georgy** Control your Meris Mercury7 pedal with your web browser. View all the pedal's settings at once. * ### [MicroFreak Reader](https://studiocode.dev/doc/microfreak-reader/) Created by: **François Georgy** An application to read and display the presets stored in the Arturia MicroFreak memory. * ### [NTS1 Web Controller](https://directions4.github.io/nts1-web-controller/) Created by: **Satoru Shikita** Web browser interface to control the KORG Nu:Tekt NTS-1. * ### [Pacer Editor](https://studiocode.dev/pacer-editor/#/) Created by: **François Georgy** Web interface for the Nektar Pacer MIDI controller. * ### [Soundshed](https://soundshed.com/) Created by: **Christopher Cook & Lavabyrd** A Desktop app to browse and manage guitar amp tones. Control your bluetooth amp, jam to video backing tracks. * ### [Sonicware Liven XFM Web Editor](https://aesqe.github.io/sonicware-liven-xfm-web-editor/) Created by: **Bruno Babić** Web editor for the Sonicware Liven XFM groovebox. ## Components & Languages * ### [FAUST](https://faust.grame.fr/) Created by: **Grame Research Lab** The online Faust IDE can be used to edit, compile and run Faust code from the Web Browser. * ### [Midi Bricks](https://midi-bricks.timsusa.vercel.app/) Created by: **Tim Susa** A tool to build custom interfaces for MIDI control. * ### [React Audio Tools](http://react-audio-tools.surge.sh/) Created by: **ambewas** A set of React components to build things with the Web Audio and Web MIDI APIs. ## Live Coding * ### [Sema](https://sema.codes/) Created by: **Francisco Bernardo, Chris Kiefer & Thor Magnusson** Sema is a playground where you can rapidly prototype live coding mini-languages for signal synthesis, machine learning and machine listening. ## Online Synthesizers * ### [synth.kitchen](https://synth.kitchen/) Created by: **Rain Rudnick** In-browser modular synthesis with Web Audio and Web MIDI. ## Notation * ### [Inscore](https://inscore.grame.fr/) Created by: **Dominique Fober, guillaumeg03 & Gabriel Leptit-Aimon** An environment for the design of interactive, augmented, dynamic musical scores. * ### [Nonoto](https://github.com/SonyCSLParis/NONOTO) Created by: **Théis Bazin, Sebastian Haas & Gaetan Hadjeres** An A.I.-powered interactive score distributed as an Electron application. It allows users to interact intuitively with any existing music composition algorithm. ## Robotics * ### [Dexter Development Environment](https://www.hdrobotic.com/software) Created by: **Haddington Dynamics** The Dexter Development Environment, which is used to control the Dexter 7-axis robot arm, bundles WEBMIDI.js for MIDI control of the device. ## SysEx Librarian * ### [Patchup](https://www.patchup.app) Created by: **Middledot Tech** Patchup is a free web app for sending, receiving, saving, and managing System Exclusive messages. All data is saved to your browser's local storage. ================================================ FILE: website/src/pages/sponsors/index.md ================================================ # Sponsors :::tip Consider a sponsorship Please help me grow and nurture WEBMIDI.js by ❤️ [sponsoring](https://github.com/sponsors/djipco) the project on GitHub. ::: ## Sponsoring Organizations --- ## Sponsors --- Anonymous Andreas Watterott ## About Software Development... As you surely know, proper software development takes time. This time is spent on coding new features but also on various other important tasks such as writing useful documentation, answering questions, reviewing issues, fixing bugs, writing tests, etc. While WEBMIDI.js started as a hobby project, it is quietly becoming the "go to" library for MIDI on the web. Hopefully, your contributions will make allow me to continue developing, maintaining and advocating for this project that I cherish. Thank you so much. 😀 ================================================ FILE: website/src/pages/tester/index.js ================================================ import React from "react"; import Layout from "@theme/Layout"; import {Helmet} from "react-helmet"; // import useBaseUrl from "@docusaurus/useBaseUrl"; // import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; // const piano = new Nexus.Piano("#target",{ // size: [500,125], // mode: "button", // "button", "toggle", or "impulse" // lowNote: 24, // highNote: 60 // }) function Tester() { return ( {/*
*/}

Edit pages/helloReact.js and save to reload.

); } export default Tester; ================================================ FILE: website/src/theme/CodeBlock/index.js ================================================ import React from 'react'; import CodeBlock from '@theme-original/CodeBlock'; export default function CodeBlockWrapper(props) { return ( <> ); } ================================================ FILE: website/src/theme/Footer/index.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import React from "react"; import {useThemeConfig} from "@docusaurus/theme-common"; import useBaseUrl from "@docusaurus/useBaseUrl"; import styles from "./styles.module.scss"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import {Helmet} from "react-helmet"; function Footer() { const {footer} = useThemeConfig(); // eslint-disable-next-line no-unused-vars const {sponsors = []} = useDocusaurusContext(); const {copyright,} = footer || {}; if (!footer) { return null; } const sponsorLogoPath = useBaseUrl("img/sponsors/edouard-montpetit-logo.svg"); return (