Repository: SeanArchibald/scale-workshop
Branch: master
Commit: f84432ca94e3
Files: 62
Total size: 2.2 MB
Directory structure:
gitextract_8o2dwx8g/
├── .editorconfig
├── .gitignore
├── .prettierrc
├── README.md
├── dev/
│ ├── docs/
│ │ └── stack.md
│ └── test/
│ └── helpers.spec.js
├── guide.htm
├── index.htm
├── src/
│ ├── assets/
│ │ └── favicon/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── css/
│ │ ├── style-dark.css
│ │ └── style.css
│ ├── js/
│ │ ├── constants.js
│ │ ├── events.js
│ │ ├── exporters.js
│ │ ├── generators.js
│ │ ├── graphics.js
│ │ ├── helpers.js
│ │ ├── keymap.js
│ │ ├── midi/
│ │ │ ├── commands.js
│ │ │ ├── constants.js
│ │ │ ├── math.js
│ │ │ ├── midi.js
│ │ │ └── ui.js
│ │ ├── modifiers.js
│ │ ├── scaleworkshop.js
│ │ ├── state/
│ │ │ ├── actions-dom.js
│ │ │ ├── actions.js
│ │ │ ├── on-ready.js
│ │ │ ├── reactions-dom.js
│ │ │ ├── reactions.js
│ │ │ └── state.js
│ │ ├── synth/
│ │ │ ├── Delay.js
│ │ │ ├── Synth.js
│ │ │ └── Voice.js
│ │ ├── synth.js
│ │ ├── ui.js
│ │ └── user.js
│ └── lib/
│ ├── bootstrap-3.3.7-dist/
│ │ ├── css/
│ │ │ ├── bootstrap-theme.css
│ │ │ └── bootstrap.css
│ │ └── js/
│ │ ├── bootstrap.js
│ │ └── npm.js
│ ├── decimal.js
│ ├── eventemitter3.js
│ ├── jquery-ui-1.12.1/
│ │ ├── AUTHORS.txt
│ │ ├── LICENSE.txt
│ │ ├── external/
│ │ │ └── jquery/
│ │ │ └── jquery.js
│ │ ├── index.html
│ │ ├── jquery-ui.css
│ │ ├── jquery-ui.js
│ │ ├── jquery-ui.structure.css
│ │ ├── jquery-ui.theme.css
│ │ └── package.json
│ └── socicon/
│ ├── Read Me.txt
│ ├── demo-files/
│ │ ├── demo.css
│ │ └── demo.js
│ ├── demo.html
│ ├── selection.json
│ ├── style.css
│ ├── style.less
│ └── variables.less
└── test.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js}]
indent_style = space
indent_size = 2
quote_type = single
max_line_length = 100
================================================
FILE: .gitignore
================================================
.vscode
================================================
FILE: .prettierrc
================================================
{
"semi": false,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none"
}
================================================
FILE: README.md
================================================
# Scale Workshop

## Description
[Scale Workshop](http://sevish.com/scaleworkshop/) allows you to design microtonal scales and play them in your web browser. Export your scales for use with VST instruments. Convert Scala files to various tuning formats.
## Frequently Asked Questions
### What kinds of microtonal scales are possible with Scale Workshop?
Scale Workshop can play any kind of microtonal scale, such as equal temperaments, just intonation, historical and traditional scales, non-octave scales, and any arbitrary tunings. The application offers a few methods to generate scales automatically based on parameters you set, or otherwise you can enter your scale data manually.
### Can I play and hear my scale?
Yes, the built-in synth allows you to play your scales within the web browser. If your browser supports web MIDI then you can use a connected MIDI device to play notes. Otherwise you can use your computer keyboard (e.g. a QWERTY keyboard) as an isomorphic keyboard controller to hear your scales. You can also play on a touch device using the 'Touch Keyboard' feature.
### Can I use Scale Workshop to tune up other synths?
Scale Workshop supports any synth that uses Scala (.scl/.kbm) files or AnaMark TUN (.tun) files. It can also export Native Instruments Kontakt tuning scripts, Max/MSP coll tuning tables and Pure Data text tuning tables.
The Xen Wiki has a [list of microtonal software plugins](https://en.xen.wiki/w/List_of_Microtonal_Software_Plugins) that support Scala and AnaMark files.
### How do I enter scale/tuning data manually?
Scale data should be entered in to the large text field labeled ‘Scale data’. Add each note on its own new line. Cents and ratios are both supported.
* To specify a ratio, simply write it in the format e.g. `3/2`
* To specify an interval in cents, include a . in the line e.g. `701.9` or `1200.`
* To specify n steps out of m-EDO, write it in the format `n\m`
No need to enter `0.` or `1/1` on the first line as your scale is automatically assumed to contain this interval.
The interval on the final line is assumed to be your interval of equivalence (i.e. your octave or pseudo-octave).
Don't add any other weird data to a line. Don't try to mix decimals with ratios (e.g. `2/1.5`). Scale Workshop will try to gracefully ignore any rubbish that you put in, but it's very possible that weird stuff will happen.
### Can I copy and paste the contents of a Scala file (.scl) directly into the 'Scale data' field?
Scala files contain non-tuning related comments at the top of the file, so Scale Workshop will throw an error if you try to paste them in directly. Instead you can use the ‘Load .scl’ function, which automatically removes those comments for you. Or you can paste the Scala file but remove the comments manually.
### Can I convert a TUN file to another format?
Yes, start by clicking New > Import .TUN and then load your TUN file into Scale Workshop. Then click Export and select your desired output format. Note that Scale Workshop is not a fully compliant AnaMark TUN v2 parser, however it should be able to read TUN files exported by Scala and Scale Workshop.
### How do I make my own keyboard mapping?
Keyboard mappings are not currently supported. You can still export a Scala keyboard mapping file (.kbm) but it will assume a linear mapping.
However you can always use duplicate lines in your tuning in order to skip any keys that you don't want to include, or write your .kbm file manually.
### Can I undo/redo?
Use your browser's Back/Forward navigation buttons to undo/redo changes to your tuning.
### How can I share my tunings with a collaborator?
Use Export > Share scale as URL. The given URL can be copied and pasted to another person. When they open the link they will see a Scale Workshop page with your scale already tuned in.
### How can I save my work for later?
You can bookmark the current page to save your work for later. This works because your tuning data is stored within the bookmarked URL.
### When I export a tuning, I get a weird filename, why?
Exporting a file with the correct filename is not supported in Safari (iOS and macOS). You can try to use Firefox, Chrome or Opera instead.
### Can I run this software offline?
Yes, just download the project from GitHub as a zip file and run index.htm from your web browser.
### Can you add a new feature to Scale Workshop?
Probably! Just add your feature request to the [issue tracker](https://github.com/SeanArchibald/scale-workshop/issues) on GitHub.
### I found a bug
Please [create a bug report](https://github.com/SeanArchibald/scale-workshop/issues) detailing the steps to reproduce the issue. You should also include which web browser and OS you are using.
## Contributing
Please base any work on develop branch, and pull requests should also be made against develop branch not master.
## Changelog
### 1.5
* Feature: output MIDI for real-time microtuning in MIDI synths that support multichannel pitch bend
* Feature: added button for toggling velocity sensing for MIDI in
* Improvement: Virtual Keyboard now works better on desktop, can be closed with the Esc key, keys are highlighted when pressed
* Improvement: user guide documentation is updated
* Improvement: better decimal precision
* Bug fix: typo in Kraig Grady Centaura Harmonic preset scale
* Bug fix: enumerate chord inversion
* Bug fix: Rotate modifier now preserves nonlinear scale data
### 1.4.2
* New modifier: Rotate. This allows you to choose an interval from your scale to be the new 1/1. (Known issue: it doesn't work as expected for intervals with decimal notation e.g. `1,5`, `2,0`)
* Bug fixed: synth notes stuck playing quietly in the background
### 1.4.1
* Bug fixed: lag on Subset option
* Bug fixed: audio drop out
* Improvement: Graphical ruler shows currently loaded scale
### 1.4
* New scale generator: Combination Product Set (CPS)
* New scale modifier: Reduce
* New scale modifier: Sort ascending
* New keyboard keymap: Colemak DH
* Bug fixed in TUN v1 file export (will improve compatibility with Serum, maybe others)
* Improvement: More synth waveforms added
* Improvement: More preset scales added
* Improvement: Updates to the user guide
* Improvement: Reaper Note Name Map provides more options
* Improvement: Rank-2 scale generator will now preserve interval notation (e.g. it will return ratios if you input ratios, return cents if you input cents)
### 1.3.2
* Many more preset scales added. Happy exploring!
### 1.3.1
* AnaMark TUN export now supports a choice of v1 or v2 as different synths require a certain version.
### 1.3
* AnaMark TUN export now contains v1 data only. This should improve compatibility with synths (e.g. Omnisphere and Quanta)
* New feature: Export Korg Minilogue & Monologue tuning formats (.mnlgtuns & .mnlgtuno)
* New feature: Export Soniccouture tuning format (.nka)
* New feature: Export Reaper Note Name Map (.txt)
* Bug fix: AnaMark TUN export is now v1 compliant - fixes compatibility with Spectrasonics Omnisphere
### 1.2
* New feature: Approximate scale by harmonics of an arbitrary denominator
* New feature: Approximate scale by subharmonics of an arbitrary numerator
* New feature: Approximate scale to equal divisions
* Improvement: 'Clear scale' function now moved into 'New' menu
* Improvement: 'Mode' renamed to 'Subset'
* Improvement: Various updates to the user guide
* Bug fix: 'Stretch/compress' now works as it should
* Bug fix: 'Tempo-sync beating' now works as it should
### 1.1.1
* Improvement: When sharing a Scale Workshop link (on Discord, Facebook, etc.) the site description is now much shorter so takes less space
### 1.1
* New feature: Export tuning files for Harmor and Sytrus synths (thanks to Azorlogh)
* Improvement: 'Mode' feature now shows a counter while you input a subset
* Improvement: Include scale URL in a comment within exported TUN, scl, Max/MSP txt and Kontakt script exports (issue #66)
### 1.0.4
* New feature: 'Approximate' method for modifying scales can produce rational approximations of your scale
* Improvement: Enumerate Chord method of scale generation now allows for inverted chords (e.g. 4:5:6 inverted would give 10:12:15)
* Improvement: site now automatically redirects to HTTPS on domains known to have valid HTTPS
* Bug fix: changing the main volume before pressing the first note no longer gets ignored
* Bug fix: exported TUN files now has a correct functional tuning section (https://github.com/SeanArchibald/scale-workshop/issues/82)
### 1.0.3
* New feature: generate scale from 'Enumerate chord' e.g. `4:5:6:7:8` will result in a scale containing intervals 1/1, 5/4, 3/2, 7/4, 2/1
* New feature: specify an interval in decimal format e.g. `1,5` for a perfect fifth, `2,0` for an octave.
* New feature: export your tuning as a list of Deflemask 'fine tune' effect parameters. The resulting text file is a reference you can use when manually inputting notes into Deflemask chip music tracker.
* Improvement: Colemak keyboard layout support added
* Improvement: when generating rank-2 temperaments, finding MOS scale sizes is now more efficient.
* Bug fix: error when changing main volume before audio initialised
### 1.0.2
* MIDI now waits for user input before initializing (issues #56 #57)
* Rank-2 temperament generator now assumes you want all positive/up generators by default (issue #58)
### 1.0.1
* Fix stuck notes during MIDI note input
* Fix stuck notes when playing pad synth in Firefox/Safari
### 1.0.0
* Stable version
* New modifier added: tempo-sync beating
* Minor bug fixes
### 0.9.9
* Added a selection of preset scales
* Fix issue using delay in some situations
* Fix issue stretching/compressing scales in some situations
* Minor interface and user guide improvements
### 0.9.8
* Fix .scl import bug
### 0.9.7
* Added user guide
* Fix `n\m` style data input
### 0.9.6
* Improved modal dialogs on mobile
* Fix regression exporting .tun files
### 0.9.5
* Loading the synth is now delayed as much as possible
* Better compatibility for exported Scala files (placeholder description will be used if user doesn't provide a tuning description)
* Improved mode input - you can optionally enter a list of scale degrees from the base note (e.g. 2 4 5 7 9 11 12)
* Stricter validation of tuning data input, improves security
* More default/auto keyboard colour layouts added
### 0.9.4
* Import AnaMark .tun files (NOT compliant to the AnaMark v2 spec, but should import tun files generated by Scala and Scale Workshop)
* Dvorak and Programmer Dvorak keyboard layouts are now supported
* Code refactoring and improvements
* Fix: Scale Workshop will no longer prevent keyboard shortcuts from being used
### 0.9.3
* Undo/redo function (via browser back/forward navigation)
* Various UI improvements, mostly for phone-sized devices
* Code refactoring and improvements (thanks Lajos)
### 0.9.2
* Added key colour customisation
* Added 'About Scale Workshop' screen
* When sharing scale by URL, key colour layout and synth options will now carry across
* When using a menu option that opens a modal dialog, the first field will automatically be selected
* Choice of regional keyboard layout is now remembered across sessions
* Delay time control now shows milliseconds value
### 0.9.1
* Improved rank-2 temperament generator. You can now specify how many generators up or down from 1/1
### 0.9
* Added virtual keyboard for touch interfaces (experimental)
### 0.8.9
* Improved workflow ('Calculate' button removed as the app now responds to scale data changes automatically)
* Improved no-javascript error message
* Fix: Scala .scl file export now preserves ratios instead of converting them to cents
### 0.8.8
* Fix stuck notes in Mozilla Firefox (due to differing implementations of the Web Audio API between web browsers, the amplitude envelopes are going to sound slightly different in Firefox)
* Fix blank option shown in 'Line endings format' when using Scale Workshop for the first time
* Fix styling issue with light theme when hovering over top menu option
### 0.8.7
* Basic MIDI input support
* General Settings are now automatically saved and restored across sessions
* Added "Night Mode" dark theme for late night sessions in the workshop
* Added user.js file where you can add your own custom script if needed
### 0.8.6
* Added info tooltips
* URL fix for Xenharmonic Wiki
### 0.8.5
* Added amplitude envelope for synth notes (organ, pad, and percussive presets)
* Added main volume control
* Added keyboard layout setting for international keyboards (English and Hungarian supported)
### 0.8.4
* Added delay effect
* Added 'auto' function for base frequency, which calculates the frequency for the specified MIDI note number assuming 12-EDO A440
* Added option to choose between Microsoft/Unix line endings
* Added indicator to show when Qwerty isomorphic keyboard is active (when typing in a text field, it is inactive)
* Added 'Quiet' button in case things get noisy in the workshop
* Added share scale as URL to email, twitter
* Fix sharing scale as URL - isomorphic mapping
* Removed debug option - debug messages will now be output to the JavaScript console by default. Use `debug = false;` in console to disable
* Improved options menu - options instantly take effect when changed (removed Apply/Save button)
### 0.8.3
* Fix sharing scale as URL - now the qwerty isomorphic mapping is correctly shared
### 0.8.2
* Settings have been moved to the right-side column (desktop)
* Added option to export a list of frequencies in text format readable by Pure Data's [text] object
### 0.8.1
* Choice of waveform for the synth: triangle, sawtooth, square, sine
* Settings menus added - General, Synth and Note Input settings
* Qwerty isomorphic keyboard mapping can be changed in the Note Input settings
* Qwerty isomorphic keyboard mapping is saved when sharing scale by URL
* Currently displayed notes are now highlighted in the tuning data table
* Fix stuck note in FireFox when pressing `/` key
* UI improvement (for large screens): tall columns are now contained within one window and individually scrollable
* Tuning data table is now displayed more compactly to show more info at once
### 0.8
* Synth added: use the QWERTY keys to play current scale
* Export a scale as a URL with the 'Share scale as URL' option
### 0.7.1
* Fix missing line breaks on Notepad and some other text editors
* Improved readme formatting (thanks suhr!)
### 0.7.0
* Scale modifiers added: ‘stretch’, ‘random variance’, ‘mode’
* Users can now input `n\m` to specify n steps out of m-EDO
* When generating a rank-2 temperament, display the scale sizes which are MOS
* Improve UI for user input, using custom modals instead of JS prompts
* Code refactored to reduce the amount of duplication
* Code is now split up over various js files so it's easier to navigate
* Change logo/favicon to square shape
### 0.6
* Generate rank-2 temperaments
### 0.5
* Fix incorrect base frequency when exporting TUN format and NI Kontakt format
* Export Scala .kbm format
### 0.4
* All dependencies (Bootstrap, jQuery etc.) now included in scaleworkshop directory
* Import Scala .scl format
* Export Scala .scl format
* Export AnaMark TUN format
* Export Native Instruments Kontakt script format
* Export Max/MSP coll format
### 0.3
* Generate equal-tempered tuning
* Generate harmonic series segment tuning
* Generate subharmonic series segment tuning
### 0.2
* Allow tuning data input to be parsed into a frequency table
### 0.1
* Initial version
## Contributors
* Sevish
* Scott Thompson
* Lajos Mészáros
* Carl Lumma
* Tobia
* Vincenzo Sicurella
* Azorlogh
## License
Copyright (c) 2017-2019 Sean Archibald
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: dev/docs/stack.md
================================================
# Stack
## Client
- Bootstrap 3.3.7 - https://getbootstrap.com/docs/3.3/
- jQuery 3.2.1 - https://api.jquery.com/
- jQuery UI 1.12.1 - https://jqueryui.com/
- Ramda 0.27.1 - https://ramdajs.com/docs/
- JSZip 3.2.1 - http://stuartk.com/jszip
## Testing
- Mocha - https://mochajs.org/
- Expect - https://jestjs.io/docs/expect
================================================
FILE: dev/test/helpers.spec.js
================================================
/* global describe, it, expect */
describe("helpers.js", () => {
describe("roundToNDecimals", () => {
it("takes 2 numbers and returns a number", () => {
expect(typeof roundToNDecimals(1, 2)).toBe("number");
});
it("rounds the second number to the precision of first number", () => {
expect(roundToNDecimals(3, 5.256846549)).toBe(5.257);
});
it("returns NaN, when non-numeric parameters were given", () => {
expect(roundToNDecimals(1, ["foo", 5, "bar"])).toBeNaN();
expect(roundToNDecimals("cat", 1.25)).toBeNaN();
});
});
describe("logModulo", () => {
it("takes 2 numbers and returns a number", () => {
expect(typeof logModulo(3, 2)).toBe("number");
});
it("returns the exponential modulus of the first number based in the second number", () => {
expect(logModulo(2/3, 2)).toBe(4/3);
expect(logModulo(45/7, 1.5)).toBe(1.26984126984127);
});
it("returns NaN, when non-numeric parameters were given", () => {
expect(logModulo(1, "foo")).toBeNaN();
expect(logModulo("cat", 1.25)).toBeNaN();
});
it("returns NaN, when first number is 0", () => {
expect(logModulo(0, 2)).toBeNaN();
});
it("returns NaN, when modulus is 0 or 1", () => {
expect(logModulo(2, 0)).toBeNaN();
expect(logModulo(2, 1)).toBeNaN();
});
});
describe("isCent", () => {
it("returns false, when given input is not a string", () => {
expect(isCent(6.52)).toBe(false);
expect(isCent([1, 2, 3])).toBe(false);
});
it("returns true, when given string is a floating point number", () => {
expect(isCent("127.052")).toBe(true);
});
it("returns true, when given string has a dot, but doesn't have any decimals written", () => {
expect(isCent("150.")).toBe(true);
});
it("returns true when given string contains whitespace around the number", () => {
expect(isCent("700.0 ")).toBe(true);
expect(isCent(" 700.0")).toBe(true);
expect(isCent(" 700.0 \t")).toBe(true);
});
it("returns false, when given string contains multiple numbers", () => {
expect(isCent("700. 500.")).toBe(false);
});
it("returns false, when the float in the given string contains whitespaces", () => {
expect(isCent("3. 141592")).toBe(false);
});
it("returns false, when given string contains no numbers", () => {
expect(isCent("hello")).toBe(false);
expect(isCent("// this is a comment")).toBe(false);
expect(isCent("")).toBe(false);
expect(isCent(" ")).toBe(false);
});
it("returns true, when negative", () => {
expect(isCent("-100.0")).toBe(true);
});
});
describe("isCommaDecimal", () => {
it("returns false, when given input is not a string", () => {
expect(isCommaDecimal(6.52)).toBe(false);
expect(isCommaDecimal([1, 2, 3])).toBe(false);
});
it("returns false, when given string is a standard floating point number", () => {
expect(isCommaDecimal("12.34")).toBe(false);
});
it("returns true, when given string is a floating point number, but with a comma replacing a point", () => {
expect(isCommaDecimal("127,052")).toBe(true);
});
it("returns true, when given string has a comma, but doesn't have any decimals written", () => {
expect(isCommaDecimal("150,")).toBe(true);
});
it("returns true when given string contains whitespace around the number", () => {
expect(isCommaDecimal("700,0 ")).toBe(true);
expect(isCommaDecimal(" 700,0")).toBe(true);
expect(isCommaDecimal(" 700,0 \t")).toBe(true);
});
it("returns false, when given string contains multiple numbers", () => {
expect(isCommaDecimal("700, 500,")).toBe(false);
});
it("returns false, when the float in the given string contains whitespaces", () => {
expect(isCommaDecimal("3, 141592")).toBe(false);
});
it("returns false, when given string contains no numbers", () => {
expect(isCommaDecimal("hello")).toBe(false);
expect(isCommaDecimal("// this is a comment")).toBe(false);
expect(isCommaDecimal("")).toBe(false);
expect(isCommaDecimal(" ")).toBe(false);
});
});
describe("isNOfEdo", () => {
it("returns true, when given input is negative", () => {
expect(isNOfEdo("-10\\12")).toBe(true);
});
});
describe("isNegativeInterval", () => {
it("returns false if input is a nonnegative ratio or decimal", () => {
expect(isNegativeInterval("3/2")).toBe(false);
expect(isNegativeInterval("1,5")).toBe(false);
expect(isNegativeInterval("1/2")).toBe(false);
expect(isNegativeInterval("0,5")).toBe(false);
});
it("returns false if cents or N of EDO is positive", () => {
expect(isNegativeInterval("1200.0")).toBe(false);
expect(isNegativeInterval("1\\12")).toBe(false);
});
it("returns true if cents or N of EDO evaluates to a negative number", () => {
expect(isNegativeInterval("-1200.0")).toBe(true);
expect(isNegativeInterval("-1\\12")).toBe(true);
});
it("returns LINE_TYPE.INVALID if ratio, decimal, or N of EDO denominator is negative", () => {
expect(isNegativeInterval("-2/1")).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval("2/-1")).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval("-1,5")).toBe(LINE_TYPE.INVALID);
});
it("returns LINE_TYPE.INVALID on invalid input", () => {
expect(isNegativeInterval("1\\-12")).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval("2-3")).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval("foo")).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval([1, 2, 3])).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval(NaN)).toBe(LINE_TYPE.INVALID);
expect(isNegativeInterval()).toBe(LINE_TYPE.INVALID);
});
})
describe("sum_array", () => {
it("takes an array of numbers and returns a number", () => {
expect(typeof sum_array([1, 2, 3, 4])).toBe("number");
});
it("sums the numbers in an array", () => {
expect(sum_array([1, 2, 3, 4])).toBe(10);
});
it("sums the numbers in an array up to a stopping index", () => {
expect(sum_array([1, 2, 3, 4], 3)).toBe(6);
});
it("returns NaN, when array contains non-numeric values", () => {
expect(sum_array([1, "foo"])).toBeNaN();
});
});
describe("rotate", () => {
it("shifts the values of an array, with wrapping indicies", () => {
expect(rotate([0, "foo", "bar", 1], 5)).toEqual([1, 0, "foo", "bar"]);
expect(rotate([0, "foo", "bar", 1], -5)).toEqual(["foo", "bar", 1, 0]);
});
});
describe("get_cf", () => {
it("takes a number and calculates it's continued fraction representation", () => {
expect(get_cf(1.25)).toEqual([1, 4]);
expect(get_cf(1 / 3)).toEqual([0, 3]);
expect(get_cf(Math.sqrt(2), 4)).toEqual([1, 2, 2, 2]);
expect(get_cf(Decimal.acos(-1))).toEqual([3, 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, 14, 2, 1]);
});
it("returns an array containing zero if set to 0 iterations", () => {
expect(get_cf(1, 0)).toEqual([0]);
});
it("round significant digits down to 0 if they are below a given precision", () => {
expect(get_cf(1 + 1e-7, 4, 6)).toEqual([1]);
expect(get_cf(Decimal.acos(-1), 15, 2)).toEqual([3, 7, 15, 1]);
});
it("returns NaN if given a non-numerical value", () => {
expect(get_cf("foo")).toBeNaN();
});
});
describe("get_convergent", () => {
it("takes an array of numbers representing a continued fraction and returns a ratio in its convergent series", () => {
expect(get_convergent([3])).toEqual("3/1");
expect(get_convergent([1, 1, 1])).toEqual("3/2");
expect(get_convergent([1, 2, 10])).toEqual("31/21");
expect(get_convergent(get_cf(Decimal.acos(-1)), 3)).toEqual("333/106");
});
it ("returns a whole number fraction if given a number instead of a continued fraction", () => {
expect(get_convergent(2)).toBe("2/1");
expect(get_convergent(0)).toBe("0/1");
})
it("returns NaN if the given array contains a non-numerical value", () => {
expect(get_convergent([1, "foo"])).toBeNaN();
expect(get_convergent(NaN)).toBeNaN();
});
})
describe("decimal_to_ratio", () => {
it("takes a decimal value and returns a ratio", () => {
// expect(decimal_to_ratio(0)).toBe("0/1");
expect(decimal_to_ratio("1.5")).toBe("3/2");
expect(decimal_to_ratio(1 / 3)).toBe("1/3");
expect(decimal_to_ratio(Decimal.acos(-1))).toBe("245850922/78256779");
});
it ("takes a commadecimal value and returns a ratio", () => {
expect(decimal_to_ratio("1,3")).toBe("13/10");
});
it("parses the ratio with given a given depth", () => {
expect(decimal_to_ratio(Decimal.acos(-1), 1)).toBe("3/1");
expect(decimal_to_ratio(Decimal.acos(-1), 2)).toBe("22/7");
expect(decimal_to_ratio(Decimal.acos(-1), 3)).toBe("333/106");
});
it("returns false if the given value is not a valid LINE_TYPE.DECIMAL", () => {
expect(decimal_to_ratio("foo")).toBe(false);
});
});
describe("cents_to_ratio", () => {
it("takes a cents value and returns its ratio representation as a string", () => {
// expect(decimal_to_ratio(0)).toBe("0/1");
expect(cents_to_ratio("1200.0")).toBe("2/1");
expect(cents_to_ratio(701.955)).toBe("3/2");
expect(cents_to_ratio(0.0)).toBe("1/1");
expect(cents_to_ratio("-498.045")).toBe("3/4");
});
it("parses the ratio with given a given depth", () => {
expect(cents_to_ratio(700.0, 2)).toBe("3/2");
expect(cents_to_ratio(550.0, 4)).toBe("11/8");
expect(cents_to_ratio(833.0903, 6)).toBe("13/8");
});
it("returns false if the given value is not a valid LINE_TYPE.CENTS", () => {
expect(cents_to_ratio("foo")).toBe(false);
});
});
describe("n_of_edo_to_ratio", () => {
it("takes an N of EDO value and returns its ratio representation as a string", () => {
expect(n_of_edo_to_ratio("0\\1")).toBe("1/1");
expect(n_of_edo_to_ratio("3\\3")).toBe("2/1");
expect(n_of_edo_to_ratio("13\\31")).toBe("1940489/1451018");
expect(n_of_edo_to_ratio("50\\72")).toBe("5193/3209");
//expect(n_of_edo_to_ratio("-24\\12")).toBe("1/4");
});
it("parses the ratio with given a given depth", () => {
expect(n_of_edo_to_ratio("13\\31", 2)).toBe("3/2");
expect(n_of_edo_to_ratio("1\\2", 3)).toBe("7/5");
expect(n_of_edo_to_ratio("50\\72", 6)).toBe("13/8");
});
it("returns false if the given value is not a valid LINE_TYPE.N_OF_EDO", () => {
expect(n_of_edo_to_ratio("foo")).toBe(false);
});
//it("return NaN when divisor is 0")
});
describe("getGCD", () => {
it("returns the largest factor of both numbers given", () => {
expect(getGCD(3, 12)).toBe("3");
expect(getGCD(7, 19)).toBe("1");
expect(getGCD(17, 51)).toBe("17");
expect(getGCD("116825103759765625", "12971141321962887")).toBe("7")
});
it("returns NaN if a non-numerical value is given", () => {
expect(getGCD(1, "foo")).toBeNaN();
});
it("returns the largest number if 0 is an argument", () => {
expect(getGCD(0, 4)).toBe("4");
});
it("returns a positive integer regardless of input signs", () => {
expect(getGCD(-1, -1)).toBe("1");
expect(getGCD(-21, 15)).toBe("3");
expect(getGCD(-4, 20)).toBe("4");
});
});
describe("ratioIsValid", () => {
it("returns true with a well-formed positive or negative integer ratio", () => {
expect(ratioIsValid("1/2")).toBe(true);
expect(ratioIsValid("-1/2")).toBe(true);
expect(ratioIsValid("1/2")).toBe(true);
expect(ratioIsValid("116825103759765625/12971141321962887")).toBe(true);
});
it('returns false if it contains a non-integer value', () => {
expect(ratioIsValid("foo")).toBe(false);
expect(ratioIsValid("foo/5")).toBe(false);
expect(ratioIsValid("5/bar")).toBe(false);
expect(ratioIsValid("2.3/5.8")).toBe(false);
expect(ratioIsValid("4")).toBe(false);
});
it('returns false if the denominator is 0', () => {
expect(ratioIsValid("1/0")).toBe(false);
});
});
describe("ratioIsSafe", () => {
it("returns true if ratio integers are 20 digits or below", () => {
expect(ratioIsSafe("3/2")).toBe(true);
expect(ratioIsSafe("123456789012345678799/1")).toBe(true);
expect(ratioIsSafe("100/123456789012345678799")).toBe(true);
});
it("returns false if ratio integers are above 20 digits", () => {
expect(ratioIsSafe("4722366482869645213696/1")).toBe(false);
expect(ratioIsSafe("1/4722366482869645213696")).toBe(false);
})
});
// Use different values from transposeSelf?
describe("powRatio", () => {
it("returns a ratio to the given power", () => {
expect(powRatio("3/2", 3)).toBe("27/8");
expect(powRatio("5/3", 23)).toBe("11920928955078125/94143178827");
expect(powRatio("2/1", -2)).toBe("1/4");
expect(powRatio("1/2", -2)).toBe("4/1");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(powRatio("3/2", 72)).toBe("50540.760062");
});
it("returns unison if power is 0", () => {
expect(powRatio("3/2", 0)).toBe("1/1");
});
it("returns NaN if given a non-numerical value", () => {
expect(powRatio("foo", 1)).toBeNaN();
expect(powRatio("2/1", "foo")).toBeNaN();
});
});
describe("simplifyRatio", () => {
it("returns a reduced ratio string given a ratio string", () => {
expect(simplifyRatio("2/4")).toBe("1/2");
expect(simplifyRatio("17/51")).toBe("1/3");
expect(simplifyRatio("0/100")).toBe("0/1");
});
it("returns a negative numerator if computed value is negative", () => {
expect(simplifyRatio("4/-4")).toBe("-1/1");
expect(simplifyRatio("-4/4")).toBe("-1/1");
});
it("returns NaN if given a non-numerical value", () => {
expect(simplifyRatio("foo")).toBeNaN();
});
it("returns NaN if given a denominator of 0", () => {
expect(simplifyRatio("1/0")).toBeNaN();
});
});
describe("periodReduceRatio", () => {
it("takes two ratios, representing an interval and period, and returns the first ratio reduced between 1 and the second ratio", () => {
expect(periodReduceRatio("1/1", "3/2")).toBe("1/1");
expect(periodReduceRatio("9/1", "2/1")).toBe("9/8");
expect(periodReduceRatio("3/2", "3/2")).toBe("1/1");
expect(periodReduceRatio("5/4", "16/15")).toBe("16875/16384");
expect(periodReduceRatio("3/4", "3/2")).toBe("9/8");
expect(periodReduceRatio("16677181699666569/17179869184", "2/1")).toBe("16677181699666569/9007199254740992");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(periodReduceRatio("22528399544939174411840147874772641/4722366482869645213696", "2/1")).toBe("140.760062");
expect(periodReduceRatio("444141444444/222211123134124", "1770695989828143/1124948488149")).toBe("1984.254914")
})
it("returns NaN if given a non-numerical value", () => {
expect(periodReduceRatio("foo", "2/1")).toBeNaN();
expect(periodReduceRatio("1/1", "foo")).toBeNaN();
});
it("returns NaN if given a denominator of 0", () => {
expect(periodReduceRatio("1/0", "2/1")).toBeNaN();
expect(periodReduceRatio("1/1", "1/0")).toBeNaN();
});
it("returns NaN if a ratio is 0", () => {
expect(periodReduceRatio("0/1", "2/1")).toBeNaN();
expect(periodReduceRatio("2/1", "0/1")).toBeNaN();
});
it("returns NaN if the mod ratio is 0 or 1", () => {
expect(periodReduceRatio("2/1", "0/1")).toBeNaN();
expect(periodReduceRatio("2/1", "1/1")).toBeNaN();
});
});
describe("transposeRatios", () => {
it("takes two ratios and returns their simplified product", () => {
expect(transposeRatios("1/1", "3/2")).toBe("3/2");
expect(transposeRatios("3/2", "3/2")).toBe("9/4");
expect(transposeRatios("5/4", "16/15")).toBe("4/3");
expect(transposeRatios("9008000000000001/8675309000000001", "17/16")).toBe("51045333333333339/46268314666666672");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(transposeRatios("22528399544939174411840147874772641/4722366482869645213696", "5/191581231380566414401")).toBe("-27524.747979")
expect(transposeRatios("5/191581231380566414401", "22528399544939174411840147874772641/4722366482869645213696")).toBe("-27524.747979")
});
// it("returns a negative numerator if computed value is negative", () => {
// expect(simplifyRatioString("4/-4")).toBe("-1/1");
// expect(simplifyRatioString("-4/4")).toBe("-1/1");
// });
it("returns NaN if given a non-numerical value", () => {
expect(transposeRatios("foo")).toBeNaN();
});
it("returns NaN if given a denominator of 0", () => {
expect(transposeRatios("1/0")).toBeNaN();
});
});
describe("transposeNOfEdos", () => {
it("takes two n-of-EDO values and returns their sum", () => {
expect(transposeNOfEdos("1\\12", "1\\12")).toBe("2\\12");
expect(transposeNOfEdos("12\\22", "-3\\22")).toBe("9\\22");
expect(transposeNOfEdos("1\\5", "1\\7")).toBe("12\\35");
expect(transposeNOfEdos("3\\8", "5\\12")).toBe("19\\24");
});
// it("returns a negative numerator if computed value is negative", () => {
// expect(transposeNOfEdos("4/-4")).toBe("-1/1");
// expect(transposeNOfEdos("-4/4")).toBe("-1/1");
// });
it("returns NaN if given a non-numerical value", () => {
expect(transposeNOfEdos("foo")).toBeNaN();
});
it("returns NaN if given a denominator of 0", () => {
expect(transposeNOfEdos("1/0")).toBeNaN();
});
});
describe("transposeLine", () => {
it("takes two generic interval values and returns their combination, preserving the first interval type when possible", () => {
expect(transposeLine("100.0", "200.0")).toBe("300.000000");
expect(transposeLine("100.0", "7\\12")).toBe("800.000000");
expect(transposeLine("100.0", "4/3")).toBe("598.044999");
expect(transposeLine("100.0", "1,25")).toBe("486.313714");
expect(transposeLine("1\\12", "1\\6")).toBe("3\\12");
expect(transposeLine("12\\12", "2,0")).toBe("24\\12");
expect(transposeLine("1,25", "1,3")).toBe("1,625000");
expect(transposeLine("1,25", "13/10")).toBe("1,625000");
expect(transposeLine("1,25", "300.0")).toBe("1,486509");
expect(transposeLine("1,25", "1\\4")).toBe("1,486509");
expect(transposeLine("3/2", "4/3")).toBe("2/1");
expect(transposeLine("4/3", "1,5")).toBe("2/1");
expect(transposeLine("9008000000000001/8675309000000001", "5/2")).toBe("15013333333333335/5783539333333334");
expect(transposeLine("9008000000000001/8675309000000001", "1,5")).toBe("9008000000000001/5783539333333334");
});
it("transposes downward with a negative cents or N Of EDO transposer", () => {
expect(transposeLine("300.0", "-100.0")).toBe("200.000000");
expect(transposeLine("1\\4", "-100.0")).toBe("200.000000");
expect(transposeLine("3/1", "-1200.0")).toBe("3/2");
expect(transposeLine("3,0", "-1200.0")).toBe("1,500000");
expect(transposeLine("300.0", "-1\\12")).toBe("200.000000");
expect(transposeLine("1\\4", "-1\\12")).toBe("2\\12");
expect(transposeLine("3/1", "-12\\12")).toBe("3/2");
expect(transposeLine("3,0", "-12\\12")).toBe("1,500000");
});
it("allows for negative cents & N Of Edos when transposed below unison", () => {
expect(transposeLine("100.0", "1/2")).toBe("-1100.000000");
expect(transposeLine("100.0", "0,5")).toBe("-1100.000000");
expect(transposeLine("100.0", "-12\\12")).toBe("-1100.000000");
expect(transposeLine("100.0", "-1200.0")).toBe("-1100.000000");
expect(transposeLine("1\\12", "1/2")).toBe("-11\\12");
expect(transposeLine("1\\12", "0,5")).toBe("-11\\12");
expect(transposeLine("1\\12", "-12\\12")).toBe("-11\\12");
expect(transposeLine("1\\12", "-1200.0")).toBe("-11\\12");
});
it("preserves decimal if combined with N of EDO", () => {
expect(transposeLine("12\\12", "1,5")).toBe("3,000000");
expect(transposeLine("1\\12", "1,5")).toBe("1,589195");
});
it("returns cents if N of EDO is combined with cents or ratio", () => {
expect(transposeLine("1\\12", "3/2")).toBe("801.955001");
expect(transposeLine("1\\12", "700.0")).toBe("800.000000");
});
it ("returns cents when a ratio is combined with N of EDO or cents", () => {
expect(transposeLine("2/1", "1\\12")).toBe("1300.000000");
expect(transposeLine("2/1", "700.0")).toBe("1900.000000");
expect(transposeLine("9008000000000000/8675309000000001", "700.0")).toBe("765.150019");
expect(transposeLine("9008000000000000/8675309000000001", "7\\12")).toBe("765.150019");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(transposeLine("22528399544939174411840147874772641/4722366482869645213696", "5/191581231380566414401")).toBe("-27524.747979");
expect(transposeLine("5/191581231380566414401", "22528399544939174411840147874772641/4722366482869645213696")).toBe("-27524.747979");
expect(transposeLine("22528399544939174411840147874772641/4722366482869645213696", "3,2")).toBe("52554.446348");
expect(transposeLine("22528399544939174411840147874772641/20769187434139310514121985316880384", "60.0")).toBe("200.760062");
expect(transposeLine("22528399544939174411840147874772641/4722366482869645213696", "7\\12")).toBe("51240.760062");
});
// it("returns a negative numerator if computed value is negative", () => {
// expect(transposeLine("4/-4")).toBe("-1/1");
// expect(transposeLine("-4/4")).toBe("-1/1");
// });
it("returns NaN if given a non-numerical value", () => {
expect(transposeLine("foo")).toBeNaN();
});
it("returns NaN if given a denominator of 0", () => {
expect(transposeLine("1/0")).toBeNaN();
});
it("returns 1,0 if transposing a commadecimal by its negation", () => {
expect(transposeLine("2,0", negateLine("2,0"))).toBe("1,000000");
});
});
describe("transposeSelf", () => {
it("returns the interval produced from stacking itself a number of times ", () => {
expect(transposeSelf("100.0", 3)).toBe("300.000000");
expect(transposeSelf("3/2", 3)).toBe("27/8");
expect(transposeSelf("1,5", 2)).toBe("2,250000");
expect(transposeSelf("3\\31", 2)).toBe("6\\31");
expect(transposeSelf("5/3", 23)).toBe("11920928955078125/94143178827");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(transposeSelf("3/2", 72)).toBe("50540.760062");
});
it("returns unison if stacked 0 times", () => {
expect(transposeSelf("100.0", 0)).toBe("0.000000");
expect(transposeSelf("3/2", 0)).toBe("1/1");
expect(transposeSelf("1,5", 0)).toBe("1,000000");
expect(transposeSelf("3\\31", 0)).toBe("0\\31");
});
it("returns an inverted interval if transposed a negative amount of times", () => {
expect(transposeSelf("123.4", -2)).toBe("-246.800000");
expect(transposeSelf("-100.0", -2)).toBe("200.000000");
expect(transposeSelf("2/1", -2)).toBe("1/4");
expect(transposeSelf("1/2", -2)).toBe("4/1");
expect(transposeSelf("1,25", -2)).toBe("0,640000");
expect(transposeSelf("0,5", -2)).toBe("4,000000");
expect(transposeSelf("19\\31", -2)).toBe("-38\\31");
expect(transposeSelf("-3\\31", -2)).toBe("6\\31");
})
it("returns NaN if given a non-numerical value", () => {
expect(transposeSelf("foo", 1)).toBeNaN();
expect(transposeSelf("2/1", "foo")).toBeNaN();
});
});
describe("moduloLine", () => {
it("returns the remaining interval when multiples of the modulo value are removed from the line interval, retaining line's type if possible", () => {
expect(moduloLine("100.0", "1200.0")).toBe("100.000000");
expect(moduloLine("1300.0", "1200.0")).toBe("100.000000");
expect(moduloLine("100.0", "7\\12")).toBe("100.000000");
expect(moduloLine("800.0", "7\\12")).toBe("100.000000");
expect(moduloLine("1300.0", "2/1")).toBe("100.000000");
expect(moduloLine("1300.0", "2,0")).toBe("100.000000");
expect(moduloLine("8\\12", "11\\12")).toBe("8\\12");
expect(moduloLine("61\\12", "1200.0")).toBe("1\\12");
expect(moduloLine("61\\12", "2/1")).toBe("1\\12");
expect(moduloLine("61\\12", "2,0")).toBe("1\\12");
expect(moduloLine("1,25", "1,3")).toBe("1,250000");
expect(moduloLine("1,5", "1,25")).toBe("1,200000");
expect(moduloLine("3,0", "1200.0")).toBe("1,500000");
expect(moduloLine("3,0", "12\\12")).toBe("1,500000");
expect(moduloLine("3,0", "2/1")).toBe("1,500000");
expect(moduloLine("1,7", "3/2")).toBe("1,133333");
expect(moduloLine("12/8", "4/3")).toBe("9/8");
expect(moduloLine("3/1", "2,0")).toBe("3/2");
expect(moduloLine("3/1", "1200.0")).toBe("3/2");
expect(moduloLine("3/1", "12\\12")).toBe("3/2");
expect(moduloLine("16677181699666569/17179869184", "2/1")).toBe("16677181699666569/9007199254740992");
expect(moduloLine("16677181699666569/17179869184", "2,0")).toBe("16677181699666569/9007199254740992");
expect(moduloLine("22528399544939174411840147874772641/4722366482869645213696", "1,5")).toBe("1/1");
});
it("returns an octave-based interval if number is below unison", () => {
expect(moduloLine("0,5", "2/1")).toBe("1,000000");
expect(moduloLine("2/3", "2/1")).toBe("4/3");
expect(moduloLine("3/4", "3/2")).toBe("9/8");
});
it ("returns a positive interval if the line is negative cents or N of EDO", () => {
expect(moduloLine("-100.0", "2/1")).toBe("1100.000000");
expect(moduloLine("-3\\7", "2/1")).toBe("4\\7");
});
it("returns LCM EDO if two N of EDOs are combined", () => {
expect(moduloLine("8\\12", "3\\6")).toBe("2\\12");
expect(moduloLine("4\\5", "3\\7")).toBe("13\\35");
});
it("returns decimal if combined with N of EDO", () => {
expect(moduloLine("1\\12", "1,5")).toBe("1,059463");
expect(moduloLine("9\\12", "1,5")).toBe("1,121195");
});
it("returns cents if N of EDO is combined with cents or ratio", () => {
expect(moduloLine("1\\12", "700.0")).toBe("100.000000");
expect(moduloLine("9\\12", "3/2")).toBe("198.044999");
});
it ("returns cents when a ratio is combined with N of EDO or cents", () => {
expect(moduloLine("2/1", "1\\12")).toBe("0.000000");
expect(moduloLine("2/1", "700.0")).toBe("500.000000");
expect(moduloLine("16677181699666569/17179869184", "7\\12")).toBe("66.470029");
expect(moduloLine("16677181699666569/17179869184", "700.0")).toBe("66.470029");
});
it("returns cents if a ratio integer exceeds 20 places", () => {
expect(moduloLine("22528399544939174411840147874772641/4722366482869645213696", "2/1")).toBe("140.760062");
expect(moduloLine("22528399544939174411840147874772641/4722366482869645213696", "1,41421356237")).toBe("140.760063");
expect(moduloLine("22528399544939174411840147874772641/4722366482869645213696", "1000.0")).toBe("540.760062");
expect(moduloLine("22528399544939174411840147874772641/4722366482869645213696", "4\\31")).toBe("63.340707");
});
it("returns NaN if the modLine evaluates to a decimal below 1", () => {
expect(moduloLine("3/2", "1/2")).toBeNaN();
expect(moduloLine("3/2", "-100.0")).toBeNaN();
expect(moduloLine("3/2", "-4\\7")).toBeNaN();
expect(moduloLine("3/2", "0,5")).toBeNaN();
});
it("returns NaN if given a non-numerical value", () => {
expect(moduloLine("foo", "2/1")).toBeNaN();
expect(moduloLine("2/1", "foo")).toBeNaN();
});
});
});
================================================
FILE: guide.htm
================================================
Scale Workshop User Guide
Scale Workshop
User guide 1.5
Scale Workshop allows you to design microtonal scales and play them in your web browser. Export your
scales for use with VST instruments. Convert Scala files to various tuning formats.
Top bar: access the main functions of the software
Left column: manual entry of scale data
Center column: table of tuning data
Right column: options and configuration
The top bar provides many useful functions.
New: generate a new scale or load one from a file
Modify: apply some transformation to a scale that is currently loaded
Export: save your work in one of many formats
About: about Scale Workshop
Import/export tunings
Import
Scale Workshop supports import of Scala .scl files, AnaMark .tun files and Korg 'logue .mnlgtuns/.mnlgtuno'.
Click 'New' on the top menu bar and then click 'Import .scl' or 'Import .tun'
Note: AnaMark .tun import is incomplete, but it should be able to import .tun V2.00 files exported by
Scale Workshop and Scala.
Convert
Convert .scl or convert .tun tunings by importing them and then exporting.
Export
Many synthesizers support microtonal scales. Usually they will require some format of tuning file. Scale Workshop
supports many of the popular formats.
If your synth supports .tun format, then please refer to its manual to find how to load the .tun file.
Scala scale (.scl)
If your synth supports .scl format, then please refer to its manual to find how to load the .scl file.
Scala keyboard mapping (.kbm)
Scale Workshop does not support arbitrary keyboard mappings, however it can output a .kbm file with linear
mapping that has your chosen base MIDI note and base frequency.
Max/MSP coll (.txt)
When building synths in Max/MSP try using a coll object rather than an mtof object. Coll can load a
specially-formatted text file containing a list of frequencies. Scale Workshop exports this kind of text file for
you. Set up your coll object as below, and click the 'read' button to bring up a load file dialog.
Then when you input MIDI note numbers to the coll object, it will output the desired frequency.
PureData text (.txt)
When building synths in PureData try using a text object rather than an mtof object. The text object can load a
specially-formatted text file containing a list of frequencies. Scale Workshop exports this kind of text file for
you. Then when you input MIDI note numbers to the text object, it will output the desired frequency.
Kontakt script (.txt)
Kontakt's scripting environment allows you to retune each key to any pitch. Scale Workshop can export this script
for you automatically. Once exported, copy and paste the contents of the script into the script editor of your
Kontakt instrument.
Soniccouture tuning file (.nka)
Tuning format for Soniccouture's sampled instruments for Kontakt.
Harmor Pitch Map (.fnv)
Harmor allows retuning keys from C0 to C10 for up to 5 octaves up or down from 12 EDO.
To do so, go to the "Pitch" tab, then "Keyboard mapping".
Click the small arrow at the bottom left and select "Open State File...".
From there, open the file that you exported from Scale Workshop.
You will have to do this for every part that you use (A and/or B).
Sytrus Pitch Map (.fnv)
Sytrus allows retuning keys from C0 to C10 for up to 4 octaves up or down from 12 EDO.
Warning: You won't be able to slide notes. If you do, the pitch will be incorrect.
To do so, go to operator you want to retune, select the "Pitch" tab, then "Keyboard mapping".
Click the small arrow at the bottom left and select "Open State File...".
From there, open the file that you exported from Scale Workshop.
Then, turn PE (Pitch Envelope) knob to the maximum value.
These are for the Sound Librarian software associated with Korg's Monologue, Minilogue, and Prologue synths,
and can be loaded with the "File > Load Single Scale" menu option. The "Scale" format follows the MIDI 1.0
"Bulk Tuning Dump" specification that allows for a 128-note table defined in semitones plus .0061 cents
of precision with a supported frequency range of 8.1758 Hz to 12543.875 Hz. The "Octave" format follows
the "Scale/Octave Tuning Dump" MIDI specification and only allows for 12 defined notes that will automatically
repeat at the octave. Scales with more than 12 notes will be cut off, and pitches can only deviate from standard
tuning by -64/+63 cents.
Deflemask 'fine tune' reference (.txt)
Exports a reference sheet to help you make microtonal music using the Deflemask tracker. Notes in Deflemask can have a 'fine tune' property. This reference sheet will show you the data you need to input.
Reaper Note Name Map (.txt)
Reaper allows all 128 rows in its piano roll MIDI editor to have a custom label. This export option gives
each note a label based on its pitch, with some other properties depending on the selected options.
"Scala Data" will preserve the notes' notation, keeping ratios when possible and allowing for mixed notations.
For "Cents", "Frequencies", and "Decimals", each interval will be converted to the chosen format. "Degrees"
will simply use "N\D" notation where N is the scale degree and D is the number of notes
the scale has.
Show Period Number will put the octave/period number of the interval after the pitch label.
Calculate Period in Pitch will transpose each interval by the number of periods it is from the base note.
It will try to preserve the notation of the interval, but will fallback to cents if it's not possible.
Base Period Number will shift the base note from unison by this many periods. For example if you want your
the base note & frequency to be note 48 at 256 Hz, but want unison to be an octave higher with note 60 at 512 Hz,
you can set this to -1 period, meaning the base note & frequency is shifted one period below unison.
Base Cents Value and Base Degree Number allow you to set a different value for the base note in
cents or degrees respectively, similar to the "Base Period Number" option. In most cases this should be 0.
Reaper will look for these files by default in:
Windows - C:\Users\[username]\AppData\Roaming\REAPER\MIDINoteNames
A shortcut to the REAPER folder is in Reaper's Option menu, "Show REAPER resource folder in explorer/finder".
To use a note name map, select the "Named notes" view in the MIDI editor (the option next to the piano icon
in the toolbar) and load the file with "File > Note/CC names > Load Note/CC names from file".
Note: These are only labels - you must retune the synth to hear the displayed notes in the piano roll.
Presets
A selection of preset scales are provided as examples.
Scale design
Scale Workshop allows you to create musical scales either by manual data input or by selecting menu options.
We'll cover the menu options first.
Start by clicking the New option on the top menu bar. Select from equal temperament, rank-2
temperament, etc.
Equal temperaments
Equal temperaments are scales where every
step is of equal size. The most well-known example perhaps is 12-tone equal temperament (12edo) which divides an
octave into 12 equal parts and is the standard tuning in the West. Other well known equal tempered scales include
24edo (quartertones), 19edo and 31edo.
To create a new equal temperament scale click New > Equal temperament and then enter values
to create the scale.
Number of divisions refers to the number of notes in your scale. Interval to divide will usually be 2/1 (octave)
but could be a different value, for example to make Bohlen-Pierce you would have 13 divisions of 3/1.
Rank-2 temperaments
Create a rank-2 temperament using a generator and period.
Harmonic series segments
Generate a segment of the harmonic series by specifying the lowest and highest harmonics to be part of the scale.
If the highest harmonic is double that of the lowest harmonic then the scale will repeat at the octave.
Subharmonic series segments
Generate a segment of the subharmonic series by specifying the lowest and highest subharmonics to be part of the
scale. If the highest subharmonic is double that of the lowest subharmonic then the scale will repeat at the
octave.
Enumerate chord
Create a scale from a list of harmonics separated by colons. E.g. 4:5:6:7:8
Combination product set
Create a scale by multiplying harmonics in different combinations. Optionally, the resulting scale can be reduced by 2/1 and sorted in ascending order.
Scales can be written manually by typing them in to the Scale data text field. Enter one
interval per line. Do not enter 1/1 on the first line as it is already assumed that the first note is 1/1.
Values with a . are cents values: e.g. 701.955
Values with slash (/) are ratios: e.g. 3/2
Values with a backslash (n\m) are n degrees of m-EDO: e.g. 7\12
Values with comma (,) are decimals: e.g. 1,5
The final value is your octave or pseudo-octave: e.g. 2/1
You can combine any of the above styles in the same scale if needed:
1,2
3\9
700.1
2/1
Modify scales
There are menu options to help you modify your current scale. Note you must have some scale data loaded already
in order to modify it. Click 'Modify' on the top menu bar to see the options.
Sort ascending
Sorts your scale ascendingly.
Reduce
Makes your entire scale fit within a desired 'modulus' interval, typically 2/1. Intervals in your scale which are larger than the modulus will wrap around, leaving only a remainder.
Rotate
Allows you to choose an interval of your scale to become 1/1.
Subset
Takes a subset of the current scale. Subsets are entered numerically.
Stretch/compress
Applies a linear stretch across the current scale. Enter a stretch factor, where 1 is no stretch at all.
Random variance
Applies a random detuning of each note in the scale. If the checkbox is ticked, this will also detune the octave.
If unticked, the octave will remain unchanged.
Tempo-sync beating
This retunes each note of your scale to the nearest harmonic and then tunes the base frequency to match the BPM.
This can result in LFO-like pulsations when playing chords.
The effect is most effective when using "rich" harmonic sounds (e.g. sawtooth waves) on a synth with excellent
intonation accuracy (e.g. almost all digital synths).
Resolution allows you to control how much retuning is applied. Low values will result in a more drastic retuning
of your original scale; in some cases this will result in notes converging on the same note, resulting in
duplicates. For more obvious tempo-synced effects, choose a resolution that is a power of 2, e.g. 32, 64, or 128.
Approximate by ratios
This steps you through each interval of your scale, shows you a selection of ratios that are close to your interval, and allows you to choose the ratio for each.
Approximate by harmonics
This retunes each interval in your scale to the nearest harmonic. You select the denominator.
Approximate by subharmonics
This retunes each interval in your scale to the nearest subharmonic. You select the numerator.
Equalize
This retunes each interval in your scale to the nearest equal step. You select the number of steps to equally divide the interval of equivalence.
Synthesizer
Scale Workshop has a built-in synth so that you can play and hear your scales. This is useful for auditioning the
scale you're working on, however it is very simple and isn't recommended for live performance.
Playing with a MIDI controller
Play notes on any connected MIDI device in order to hear your current scale. This requires you to use a web-MIDI capable browser. Must be enabled first by clicking
the 'MIDI I/O' menu option then clicking the 'MIDI on' button while your controller is connected.
Playing with QWERTY
Use your computer keyboard to play your current scale, much like an isomorphic keyboard.
Note: QWERTY note input is disabled any time that a text or input field has focus. An indicator
can be seen at the top-right of the screen to show you if you are able to play the QWERTY keyboard currently:
Not all international keyboard layouts are supported. Users who wish to see their keyboard layout supported are
encouraged to contribute to the project by adding their own layout to keymap.js in the project source.
Playing with mouse / touch screen
On the top menu, click Virtual Kbd to display a grid overlay. Click / Touch the grid to hear your scale. The mapping on the
grid is the same as the mapping on the QWERTY keys.
Isomorphic mapping
When using the QWERTY or the Virtual keyboard, by default each note along a horizontal axis is 1 scale degree
apart and each note along vertical axis is 5 scale degrees apart. This can be changed by using the
Isomorphic keyboard settings shown on the right column of the Scale Workshop interface.
Amplitude envelope: Organ, Pad, Percussive (Short, Medium and Long)
Delay effect: feedback delay echo
Max polyphony: maximum number of voices that can be played at one time. Use a low value if you're on a low powered device. Many devices will be capable of 64 or more simultaneous voices. Must refresh the page for changes to take effect.
MIDI I/O
Scale Workshop can behave like a MIDI tuning box, allowing for MIDI input into the app and then outputting MIDI notes plus multichannel pitch bend messages, thereby allowing you to hear your scale on a supported MIDI synth.
Before getting started, make sure that your web browser is web-MIDI compatible. Not all browsers support MIDI I/O.
On your MIDI synth device, you should set pitch bend range to +/- 1 octave.
Connect your MIDI device(s) to your computer, then click the MIDI I/O option in the top menu. Then click the red 'MIDI on' button. If this was successful, the button will turn green and you will see a list of your MIDI devices below.
MIDI devices can be enabled or disabled by selecting the check boxes next to each device. Each individual channel can also be enabled or disabled by the check boxes.
Misc. tips
General settings can be found on the right side of the Scale Workshop interface.
If an exported tuning file doesn't seem to load into a softsynth properly, then try changing the value of
Line endings format and re-saving the file. If you're using a Windows computer it's recommended
to set this to Microsoft, otherwise set this to Unix.
Dark mode makes the Scale Workshop interface dark.
Undo/redo your tuning changes by using the back/forward browser navigation buttons.
If the synth gets too noisy, you can kill all sound by clicking the Quiet button.
Play your scale using your computer keyboard or the virtual keyboard.
(ignore MOS with steps smaller than cents)
This applies a stretching or compression evenly across the whole scale. Entering 1 will cause no change;
entering 2 will make every interval twice as large.
This will add a random amount of detuning to each note of the scale.
Select a subset from the current scale.
Select a ratio that approximates the interval.
Note: if you enter an odd number, the interval of equivalence will change (e.g. mistuned octaves)
Divides your interval of equivalence into an equal number of steps, then rounds each interval in your scale to the nearest equal step.
Makes your entire scale fit within a desired 'modulus' interval, typically 2/1. Intervals in your scale which are larger than the modulus will wrap around, leaving only a remainder.
Rotates the mode of your scale.
The resulting scale will be sorted ascendingly.
Share your scale using the sharing link.
Select the options for the note map.
Because there are more than 12 notes
Sevish, Lajos Mészáros, Vincenzo Sicurella, Lumi Pakkanen, Scott Thompson, Carl Lumma, Tobia, Azorlogh
================================================
FILE: src/assets/favicon/browserconfig.xml
================================================
#00a300
================================================
FILE: src/assets/favicon/manifest.json
================================================
{
"name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: src/css/style-dark.css
================================================
/* "Night Mode" dark theme styles */
body.dark {
background-color: #000;
color: #bbb;
}
.dark p, .dark label {
color: #bbb;
}
.dark hr {
border-color: #222;
}
.dark .ui-tooltip {
background-color: #000;
}
.dark #header-mobile {
background-color: #111;
}
.dark canvas {
filter: invert(1);
}
/* Interactable elements */
.dark .navbar button, .dark .navbar-inverse .navbar-toggle:focus, .dark .navbar-inverse .navbar-toggle:hover, .dark .navbar-collapse {
background-color: #111;
}
.dark input:not(.btn), .dark textarea, .dark select {
background-color: #222;
color: #bbb;
border-color: #888;
}
.dark .input-group-addon {
background-color: #000;
color: #bbb;
}
.dark .btn-default, .dark .ui-button {
background-color: #222;
color: #bbb;
}
/* Accordion menu */
.dark .ui-accordion-header {
background-color: #222;
color: #bbb;
}
.dark .ui-accordion-content {
background-color: #000;
color: #bbb;
}
/* Navbar menu */
.dark ul.dropdown-menu {
background-color: #000;
border: 1px solid #444;
border-top: none;
}
.dark .dropdown-menu > li > a {
color: #bbb;
}
.dark .dropdown-menu>li>a:focus, .dark .dropdown-menu>li>a:hover {
background-color: #222;
}
.dark .dropdown-menu .divider {
background-color: #222;
}
.dark .ui-widget-overlay {
background: black;
}
/* Modal dialogs */
.dark .ui-widget-content {
color: #bbb;
}
.dark .ui-dialog a {
color: #ddd;
}
.dark .ui-dialog {
box-shadow: #444 0px 0px 70px;
}
.dark .ui-dialog, .dark .ui-dialog-titlebar {
background-color: #000;
color: #bbb;
border: none;
border-radius: 0px;
}
.dark .ui-dialog-titlebar {
border-bottom: 1px solid #888;
}
.dark button.ui-button.ui-corner-all.ui-widget.ui-button-icon-only.ui-dialog-titlebar-close { /* modal close buttons */
background: #000;
border: none;
}
.dark .ui-dialog-buttonpane {
background: #000;
border: none;
}
.dark .socicon-mail {
color: #bbb;
}
/* Virtual Keyboard */
.dark #virtual-keyboard {
background-color: black;
}
.dark #virtual-keyboard td {
border: 1px solid grey;
}
/* Tuning Table */
.dark #tuning-table th, .dark #tuning-table td {
border-color: #333;
}
.dark #tuning-table th:hover, .dark #tuning-table tr:hover {
background-color: #222;
}
.dark #tuning-table tr.warning td {
background-color: #192d37; /*#fcf8e3*/
}
.dark #tuning-table tr.info td {
background-color: #4c4823; /*#d9edf7*/
}
.dark tr.bg-playnote td {
background-color: #1f4018 !important; /*#dff0d8*/
}
/*
* NON-MOBILE
*/
@media (min-width: 768px) {
.dark .navbar {
background-color: #111;
}
}
@media (min-width: 992px) {
}
@media (max-width: 991px) {
}
================================================
FILE: src/css/style.css
================================================
img,
canvas {
max-width: 100%;
}
.helpicon {
color: #999;
font-size: 0.9em;
}
.hidden {
display: none;
}
.ui-widget-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
opacity: 0.5;
}
/* Make jQuery UI colors match with Bootstrap */
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default,
.ui-button,
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
color: black;
background-color: #eee;
border-color: #ccc;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
color: black;
background-color: #eee;
border-color: #ccc;
}
h3.ui-accordion-header {
font-weight: bold;
}
textarea {
resize: vertical;
}
/* normal cursor when hovering over navbar */
.navbar a {
cursor: default;
}
.navbar {
z-index: 4;
position: fixed;
left: 0px;
top: 0px;
border: none;
width: 100%;
}
.navbar button {
margin: 0px;
border-radius: unset;
border: none;
background-color: #333;
width: 50px;
}
.navbar-toggle .icon-bar {
width: 30px;
}
.navbar-inverse {
background-color: unset;
}
.navbar-toggle {
padding: 10px;
}
.navbar-toggle .icon-bar + .icon-bar {
margin-top: 12px;
}
.navbar-header {
padding-left: 0px !important;
}
.navbar-collapse {
background-color: #333;
border: none;
width: calc(100vw - 50px);
position: fixed;
top: 0px;
max-height: 100vh;
}
.navbar-brand {
display: none;
cursor: default;
}
.nav > li {
width: 49%;
display: inline-block;
}
.nav > li:hover,
.navbar-brand:hover {
background-color: #222;
}
.nav > li.open {
width: 100%;
}
#header-mobile {
background-color: #333;
width: 100%;
height: 50px;
margin-top: -20px;
}
#header-mobile h1 {
font-size: 12pt;
font-weight: normal;
color: white;
padding: 17px;
}
body > .container-fluid > .row {
margin-top: 20px;
}
input#btn_frequency_auto,
input#btn_key_colors_auto {
margin-top: 3px;
}
#txt_name {
font-size: 1.4em;
background-color: unset;
}
#col-tuning-table {
padding-left: 0px;
padding-right: 0px;
}
#tuning-table {
margin-bottom: 4px;
}
.table-condensed > tbody > tr > td,
.table-condensed > tbody > tr > th,
.table-condensed > tfoot > tr > td,
.table-condensed > tfoot > tr > th,
.table-condensed > thead > tr > td,
.table-condensed > thead > tr > th {
padding: 3px 5px;
}
tr.bg-playnote td {
background-color: #dff0d8 !important;
}
#tuning-table td,
#tuning-table th {
text-align: center;
}
p.social-icons {
text-align: center;
font-size: 1.5em;
}
.social-icons .socicon-twitter {
color: #4da7de;
}
div#qwerty-indicator {
padding: 1em;
display: none;
}
#btn_panic {
display: none;
}
div#splash {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: white;
z-index: 10;
display: table;
}
div#splash-center {
display: table-cell;
vertical-align: middle;
text-align: center;
}
div#splash-center img {
max-width: 70vw;
height: auto;
box-shadow: #aaa 0px 0px 40px;
}
#modal_load_preset_scale optgroup + optgroup {
margin-top: 1em;
}
/* Virtual keyboard */
#virtual-keyboard {
background-color: white;
position: fixed;
top: 50px;
left: 0;
width: 100vw;
min-width: 500px; /* this stops the keys getting too close together for portrait mobile users */
height: calc(100vh - 50px);
display: none;
z-index: 2;
}
#virtual-keyboard td {
text-align: center;
vertical-align: middle;
border: 1px solid grey;
font-size: 0.6em;
user-select: none;
cursor: pointer;
}
#virtual-keyboard td p {
pointer-events: none;
word-break: break-word;
line-height: 1.1em;
color: #888;
}
#virtual-keyboard .key:hover {
background: linear-gradient(
0deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 0, 0, 0.5) 50%,
rgba(255, 255, 255, 0) 100%
);
}
#virtual-keyboard .key.active {
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 255, 0, 0.5) 50%,
rgba(0, 0, 0, 0) 100%
);
}
/*
* Fullscreen variant for jQueryUI modal widget
*/
.fullscreen-modal {
top: 0px !important;
left: 0px !important;
width: 100vw !important;
height: 100vh !important;
position: fixed;
}
.fullscreen-modal .ui-dialog-buttonpane {
}
/*
* JQUERY MODAL UI MOBILE-ONLY FIXES
*/
@media (max-width: 420px), /* OR */ (max-height: 420px) {
.ui-dialog {
top: 0px !important;
left: 0px !important;
width: 100vw !important;
max-height: 100vh !important;
position: fixed;
overflow-x: scroll;
}
}
/*
* NON-MOBILE
*/
@media (min-width: 768px) {
body > .container-fluid > .row {
margin-top: 0px;
}
div#qwerty-indicator {
display: block;
}
.col-main {
/* main columns of the Scale Workshop UI */
height: calc(100vh - 70px);
overflow-y: auto;
}
#btn_panic {
display: unset;
}
#virtual-keyboard {
font-size: 0.9em;
height: calc(100vh - 50px);
}
#tuning-table td.key-color,
#tuning-table th.key-color {
border-left: 1px solid #ddd;
}
.navbar {
border-radius: 0px;
}
.navbar {
z-index: 4;
position: relative;
left: unset;
top: unset;
background-color: #222;
border: none;
}
.navbar-header {
padding-left: 0px !important;
}
.navbar-collapse {
background-color: unset;
position: unset;
}
.navbar-brand {
display: block;
}
.nav > li {
width: unset;
display: block;
}
.nav > li.open {
width: unset;
}
}
@media (min-width: 992px) {
.col-sub {
/* main columns of the Scale Workshop UI */
height: calc(100vh - 70px);
overflow-y: auto;
}
.navbar {
border-radius: 0px;
}
}
@media (max-width: 991px) {
#col-tuning-table {
margin-top: 1em;
padding-left: 0px;
padding-right: 0px;
}
}
#modal_midi_settings {
user-select: none;
}
#modal_midi_settings .form-group {
margin-top: 20px;
}
#modal_midi_settings .settings .row {
display: flex;
margin: 0;
}
#modal_midi_settings .device {
display: flex;
align-items: center;
}
#modal_midi_settings .checkbox-wrapper {
padding: 0 10px;
}
#modal_midi_settings .device input[type='checkbox'] {
margin: 0;
}
#modal_midi_settings .device h4 {
flex-grow: 1;
font-size: unset;
}
#modal_midi_settings .device h4 label {
margin: 0;
font-weight: unset;
}
#modal_midi_settings .channels {
display: flex;
flex-wrap: wrap;
}
#modal_midi_settings .channels label, #modal_midi_settings .settings label {
font-weight: unset;
}
#modal_midi_settings .device + .device {
margin-top: 2em;
}
#modal_midi_settings .channel {
display: flex;
flex-direction: column;
padding: 0 10px;
align-items: center;
}
#modal_midi_settings .channel input[type='checkbox'] {
margin: 0;
}
================================================
FILE: src/js/constants.js
================================================
const LINE_TYPE = {
CENTS: 'cents',
DECIMAL: 'decimal',
RATIO: 'ratio',
N_OF_EDO: 'n of edo',
INVALID: 'invalid'
}
const SEMITONE_RATIO_IN_12_EDO = Math.pow(2, 1 / 12)
const MNLG_OCTAVESIZE = 12
const MNLG_SCALESIZE = 128
const MNLG_MAXCENTS = 12800
const MNLG_A_REF = { val: 6900, ind: 69, freq: 440.0 }
const MNLG_C_REF = { val: 6000, ind: 60, freq: 261.6255653 }
// prettier-ignore
const PRIMES = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199,
211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397,
401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499,
503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599,
601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691,
701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797,
809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887,
907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997,
1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097,
1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193,
1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297,
1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399,
1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499,
1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597,
1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699,
1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789,
1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889,
1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999,
2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099,
2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179,
2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297,
2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399,
2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477,
2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593,
2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699,
2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797,
2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897,
2903, 2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999,
3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089,
3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191,
3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299,
3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391,
3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499,
3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593,
3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697,
3701, 3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797,
3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889,
3907, 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989,
4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093, 4099,
4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177,
4201, 4211, 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297,
4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397,
4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493,
4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597,
4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691,
4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799,
4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889,
4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999,
5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099,
5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197,
5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279, 5281, 5297,
5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399,
5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, 5471, 5477, 5479, 5483,
5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591,
5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693,
5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791,
5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897,
5903, 5923, 5927, 5939, 5953, 5981, 5987,
6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091,
6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173, 6197, 6199,
6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299,
6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397,
6421, 6427, 6449, 6451, 6469, 6473, 6481, 6491,
6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599,
6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691,
6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793,
6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899,
6907, 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997,
7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079,
7103, 7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193,
7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297,
7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393,
7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499,
7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591,
7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699,
7703, 7717, 7723, 7727, 7741, 7753, 7757, 7759, 7789, 7793,
7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883,
7901, 7907, 7919
]
================================================
FILE: src/js/events.js
================================================
/*
* EVENT HANDLERS AND OTHER DOCUMENT READY STUFF
*/
jQuery(document).ready(function () {
// automatically load generatal options saved in localStorage (if available)
if (!R.isNil(Storage)) {
// recall newline format
if (!R.isNil(localStorage.getItem('newline'))) {
jQuery('#input_select_newlines').val(localStorage.getItem('newline'))
} else {
console.log('localStorage: assuming default of windows')
jQuery('#input_select_newlines').val('windows')
}
// recall dark UI preference
if (
localStorage.getItem('night_mode') === 'true' ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
jQuery('#input_checkbox_night_mode').trigger('click')
jQuery('body').addClass('dark')
}
// recall computer keyboard layout
if (!R.isNil(localStorage.getItem('keybd_region'))) {
jQuery('#input_select_keyboard_layout').val(localStorage.getItem('keybd_region'))
synth.keymap = Keymap[localStorage.getItem('keybd_region')]
}
// recall max polyphony
if (!R.isNil(localStorage.getItem('max_polyphony'))) {
jQuery('#input_number_max_polyphony').val(localStorage.getItem('max_polyphony'))
}
} else {
console.log(
'localStorage not supported in your browser. Please check your browser settings. If using Safari, you may need to disable private browsing mode.'
)
}
// get data encoded in url
parse_url()
// base MIDI note changed
jQuery('#txt_base_midi_note').change(function () {
// update MIDI note name
jQuery('#base_midi_note_name').text(
midi_note_number_to_name(jQuery('#txt_base_midi_note').val())
)
})
// clear button clicked
jQuery('#clear-scale').click(function (event) {
event.preventDefault()
var r = confirm('Are you sure you want to clear the current tuning data?')
if (r) {
clear_all()
}
})
// auto frequency button clicked
jQuery('#btn_frequency_auto').click(function (event) {
event.preventDefault()
jQuery('#txt_base_frequency').val(mtof(jQuery('#txt_base_midi_note').val()).toFixed(6))
parse_tuning_data()
})
// import scala option clicked
jQuery('#import-scala-scl').click(function (event) {
event.preventDefault()
import_scala_scl()
})
// import anamark tun option clicked
jQuery('#import-anamark-tun').click(function (event) {
event.preventDefault()
import_anamark_tun()
})
// import mnlgtun option clicked
jQuery('#import-mnlgtun-file').on('click', function (event) {
event.preventDefault()
importMnlgtun()
})
// generate_equal_temperament option clicked
jQuery('#generate_equal_temperament').click(function (event) {
event.preventDefault()
jQuery('#input_number_of_divisions').select()
openDialog('#modal_generate_equal_temperament', generate_equal_temperament)
})
// generate_rank_2_temperament option clicked
jQuery('#generate_rank_2_temperament').click(function (event) {
event.preventDefault()
jQuery('#input_rank-2_generator').select()
openDialog('#modal_generate_rank_2_temperament', generate_rank_2_temperament)
})
// rank-2 temperament generator - generators up changed
jQuery('#input_rank-2_up').change(function () {
jQuery('#input_rank-2_down').val(
jQuery('#input_rank-2_size').val() - jQuery('#input_rank-2_up').val() - 1
)
})
// rank-2 temperament generator - scale size changed
jQuery('#input_rank-2_size').change(function () {
var size = parseInt(jQuery('#input_rank-2_size').val())
// set generators up to be one less than scale size
jQuery('#input_rank-2_up').val(size - 1)
// set generators up input maximum
jQuery('#input_rank-2_up').attr({ max: size - 1 })
// zero generators down
jQuery('#input_rank-2_down').val(0)
})
// generate_harmonic_series_segment option clicked
jQuery('#generate_harmonic_series_segment').click(function (event) {
event.preventDefault()
jQuery('#input_lowest_harmonic').select()
openDialog('#modal_generate_harmonic_series_segment', generate_harmonic_series_segment)
})
// generate_subharmonic_series_segment option clicked
jQuery('#generate_subharmonic_series_segment').click(function (event) {
event.preventDefault()
jQuery('#input_lowest_subharmonic').select()
openDialog('#modal_generate_subharmonic_series_segment', generate_subharmonic_series_segment)
})
// enumerate_chord option clicked
jQuery('#enumerate_chord').click(function (event) {
event.preventDefault()
jQuery('#input_chord').select()
jQuery('#modal_enumerate_chord').dialog({
modal: true,
buttons: {
OK: function () {
generate_enumerate_chord()
},
Cancel: function () {
jQuery(this).dialog('close')
}
}
})
})
// generate_cps option clicked
jQuery('#generate_cps').click(function (event) {
event.preventDefault()
jQuery('#input_cps_factors').select()
openDialog('#modal_generate_cps', generate_cps)
})
// load-preset option clicked
jQuery('#load-preset').click(function (event) {
event.preventDefault()
jQuery('#select_preset_scale').select()
openDialog('#modal_load_preset_scale', function () {
load_preset_scale(jQuery('#select_preset_scale')[0].value)
})
})
// modify_mode option clicked
jQuery('#modify_mode').click(function (event) {
event.preventDefault()
// setup MOS options, and hide
update_modify_mode_mos_generators()
show_modify_mode_mos_options(document.querySelector('input[name="mode_type"]:checked').value)
jQuery('#modal_modify_mos_degree').change() // make sizes available
jQuery('#input_modify_mode').select()
openDialog('#modal_modify_mode', modify_mode)
})
// modify_stretch option clicked
jQuery('#modify_stretch').click(function (event) {
event.preventDefault()
jQuery('#input_stretch_ratio').select()
openDialog('#modal_modify_stretch', modify_stretch)
})
// modify_random_variance option clicked
jQuery('#modify_random_variance').click(function (event) {
event.preventDefault()
jQuery('#input_cents_max_variance').select()
openDialog('#modal_modify_random_variance', modify_random_variance)
})
// modify_sync_beating option clicked
jQuery('#modify_sync_beating').click(function (event) {
event.preventDefault()
openDialog('#modal_modify_sync_beating', modify_sync_beating)
})
// approximate option clicked
jQuery('#modify_approximate').click(function (event) {
event.preventDefault()
// this needs to be here because a tuning data line needs to be
// inserted into the #input_interval_to_approximate field
jQuery('#txt_tuning_data').val(jQuery('#txt_tuning_data').val().trim())
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No tuning data to modify.')
return false
}
jQuery('#input_scale_degree').val(1)
jQuery('#input_scale_degree').attr({ min: 1, max: tuning_table.note_count - 1 })
jQuery('#input_scale_degree').select()
jQuery('#input_scale_degree').trigger('change')
jQuery('#modal_approximate_intervals').dialog({
modal: true,
buttons: {
Apply: function () {
modify_replace_with_approximation()
},
Close: function () {
jQuery(this).dialog('close')
}
}
})
})
// modify_approximate_harmonics option clicked
jQuery('#modify_approximate_harmonics').click(function (event) {
event.preventDefault()
jQuery('#input_approx_harm_denominator').select()
openDialog('#modal_approximate_harmonics', modify_approximate_harmonics)
})
// modify_approximate_subharmonics option clicked
jQuery('#modify_approximate_subharmonics').click(function (event) {
event.preventDefault()
jQuery('#input_approx_subharm_numerator').select()
openDialog('#modal_approximate_subharmonics', modify_approximate_subharmonics)
})
// modify_equalize option clicked
jQuery('#modify_equalize').click(function (event) {
event.preventDefault()
jQuery('#input_equalize_divisions').select()
openDialog('#modal_equalize', modify_equalize)
})
// modify_octave_reduce option clicked
jQuery('#modify_octave_reduce').click(function (event) {
event.preventDefault()
openDialog('#modal_modify_octave_reduce', modify_octave_reduce)
})
// modify_sort_ascending option clicked
jQuery('#modify_sort_ascending').click(function (event) {
event.preventDefault()
var scale = jQuery('#txt_tuning_data').val().trim().split(unix_newline)
scale = scaleSort(scale).join(unix_newline).trim()
jQuery('#txt_tuning_data').val(scale)
parse_tuning_data()
})
// modify_rotate option clicked
jQuery('#modify_rotate').click(function (event) {
event.preventDefault()
// get scale intervals
let scale = jQuery('#txt_tuning_data').val().trim().split(unix_newline)
// remove any options from drop-down
jQuery('#input_rotate_new_1_1').empty()
// loop through intervals and populate drop-down options
for (i = 0; i < scale.length - 1; i++) {
jQuery('#input_rotate_new_1_1').append('')
}
// show modal
openDialog('#modal_modify_rotate', modify_rotate)
parse_tuning_data()
})
// calculate and list rational approximations within user parameters
jQuery('#input_interval_to_approximate').change(function () {
var interval = line_to_decimal(jQuery('#input_interval_to_approximate').val())
current_approximations.convergent_indicies = []
current_approximations.numerators = []
current_approximations.denominators = []
current_approximations.ratios = []
current_approximations.numerator_limits = []
current_approximations.denominator_limits = []
current_approximations.ratio_limits = []
get_rational_approximations(
interval,
current_approximations.numerators,
current_approximations.denominators,
999999,
current_approximations.convergent_indicies,
current_approximations.ratios,
current_approximations.numerator_limits,
current_approximations.denominator_limits,
current_approximations.ratio_limits
)
modify_update_approximations()
})
// recalculate approximations when scale degree changes
jQuery('#input_scale_degree').change(function () {
jQuery('#txt_tuning_data').val(jQuery('#txt_tuning_data').val().trim())
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No tuning data to modify.')
return false
}
var index = parseInt(jQuery('#input_scale_degree').val()) - 1
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
jQuery('#input_interval_to_approximate').val(lines[index])
jQuery('#input_interval_to_approximate').trigger('change')
})
// refilter approximations when error amount changes
jQuery('#input_min_error').change(function () {
modify_update_approximations()
})
// refilter approximations when error amount changes
jQuery('#input_max_error').change(function () {
modify_update_approximations()
})
// refilter approximations when "show semiconvergents" changes
jQuery('#input_show_convergents').change(function () {
modify_update_approximations()
})
// refilter approximations when prime limit changes
// can be improved, but it's a bit tricky!
jQuery('#input_approx_min_prime').change(function () {
var num = parseInt(jQuery('#input_approx_min_prime').val())
var dif = num - PRIMES[prime_counter[0]]
if (Math.abs(dif) == 1) {
if (num < PRIMES[prime_counter[0]]) {
prime_counter[0]--
} else {
prime_counter[0]++
}
} else {
prime_counter[0] = PRIMES.indexOf(closestPrime(num))
}
jQuery('#input_approx_min_prime').val(PRIMES[prime_counter[0]])
modify_update_approximations()
})
// refilter approximations when prime limit changes
jQuery('#input_approx_max_prime').change(function () {
var num = parseInt(jQuery('#input_approx_max_prime').val())
var dif = num - PRIMES[prime_counter[1]]
if (Math.abs(dif) == 1) {
if (num < PRIMES[prime_counter[1]]) {
prime_counter[1]--
} else {
prime_counter[1]++
}
} else {
prime_counter[1] = PRIMES.indexOf(closestPrime(num))
}
jQuery('#input_approx_max_prime').val(PRIMES[prime_counter[1]])
modify_update_approximations()
})
// shows or hides MOS mode selection boxes
function show_modify_mode_mos_options(showOptions) {
document.getElementById('mos_mode_options').style.display =
showOptions == 'mos' ? 'block' : 'none'
}
jQuery('#modal_modify_mode').change(function () {
show_modify_mode_mos_options(document.querySelector('input[name="mode_type"]:checked').value)
})
// repopulates the available degrees for selection
function update_modify_mode_mos_generators() {
show_modify_mode_mos_options(document.querySelector('input[name="mode_type"]:checked').value)
let coprimes = get_coprimes(tuning_table.note_count - 1)
jQuery('#modal_modify_mos_degree').empty()
for (var d = 1; d < coprimes.length - 1; d++) {
var num = coprimes[d]
var cents = Math.round(decimal_to_cents(tuning_table.tuning_data[num]) * 10e6) / 10.0e6
var text = num + ' (' + cents + 'c)'
jQuery('#modal_modify_mos_degree').append('')
}
}
// calculate the MOS mode and insert it in the mode input box
function modify_mode_update_mos_scale() {
var p = tuning_table.note_count - 1
var g = parseInt(jQuery('#modal_modify_mos_degree').val())
var s = parseInt(jQuery('#modal_modify_mos_size').val())
let mode = get_rank2_mode(p, g, s)
jQuery('#input_modify_mode').val(mode.join(' '))
}
// update the available sizes for selection
jQuery('#modal_modify_mos_degree').change(function () {
let p = tuning_table.note_count - 1
let nn = []
let dd = []
var gp = jQuery('#modal_modify_mos_degree').val() / p
get_rational_approximations(gp, nn, dd, p)
jQuery('#modal_modify_mos_size').empty()
let size = 0
let i = 2
while (i < dd.length - 1) {
size = dd[i++]
jQuery('#modal_modify_mos_size').append('')
}
})
// update mode when size is selected
jQuery('#modal_modify_mos_size').change(function () {
modify_mode_update_mos_scale()
})
// move the mode steps back one
jQuery('#input_mode_step_left').click(function () {
var mode = jQuery('#input_modify_mode').val().split(' ')
mode = rotate(mode, -1)
jQuery('#input_modify_mode').val(mode.join(' '))
})
// move the mode steps forward one
jQuery('#input_mode_step_right').click(function () {
var mode = jQuery('#input_modify_mode').val().split(' ')
mode = rotate(mode, 1)
jQuery('#input_modify_mode').val(mode.join(' '))
})
// open dialog for Reaper named notes exporter and call with selected parameters
jQuery('#export_reaper_note_name_map').click(function (event) {
event.preventDefault()
jQuery('#input_reaper_pitch_format').trigger('change')
openDialog('#modal_reaper_named_notes', (event) => {
const pitchFormat = jQuery('#input_reaper_pitch_format').val()
const showPeriodNumbers = jQuery('#input_reaper_show_period_numbers').is(':checked')
const calculatePeriodInPitch = jQuery('#input_reaper_calculate_periods').is(':checked')
const rootPeriodNumber = parseInt(jQuery('#input_reaper_root_period').val())
const rootCentsValue = parseFloat(jQuery('#input_reaper_root_cents').val())
const rootDegreeValue = parseInt(jQuery('#input_reaper_root_degree').val())
exportReaperNamedNotes(
pitchFormat,
showPeriodNumbers,
calculatePeriodInPitch,
rootPeriodNumber,
rootCentsValue,
rootDegreeValue
)
})
})
/*
// rank-2 temperament generator - scale size changed
jQuery( '#input_rank-2_size' ).change( function() {
var size = parseInt( jQuery( '#input_rank-2_size' ).val() );
// set generators up to be one less than scale size
jQuery( '#input_rank-2_up' ).val( size - 1 );
// set generators up input maximum
jQuery( '#input_rank-2_up' ).attr({ "max" : size - 1 });
// zero generators down
jQuery( '#input_rank-2_down' ).val( 0 );
} );
*/
// Reaper Exporter interactions
// Disable 'Root Cents Value' if not using cents, and 'Root Degree Value' if not using scale degrees
jQuery('#input_reaper_pitch_format').on('change', function (event) {
jQuery('#modal_reaper_root_cents_group').css(
'display',
event.target.value === 'cents' ? 'block' : 'none'
)
jQuery('#modal_reaper_root_degree_group').css(
'display',
event.target.value === 'degree' ? 'block' : 'none'
)
})
// Disable 'Root Period Number' if not showing period numbers
jQuery('#input_reaper_show_period_numbers').on('click', function (event) {
jQuery('#input_reaper_root_period').prop('disabled', !event.target.checked)
})
// MIDI nav item clicked
jQuery('#nav_midi').click(function (event) {
event.preventDefault()
jQuery('#modal_midi_settings').dialog({
modal: true,
dialogClass: 'fullscreen-modal',
resizable: false,
draggable: false,
buttons: {
OK: function () {
jQuery(this).dialog('close')
state.set('midi modal visible', false)
}
}
})
state.set('midi modal visible', true)
})
// About Scale Workshop option clicked
jQuery('#about_scale_workshop').click(function (event) {
event.preventDefault()
jQuery('#about_version').text(APP_TITLE)
jQuery('#modal_about_scale_workshop').dialog({
modal: true,
width: 500,
buttons: {
OK: function () {
jQuery(this).dialog('close')
}
}
})
})
// Panic button
jQuery('#btn_panic').click(function (event) {
event.preventDefault()
synth.panic() // turns off all playing synth notes
})
// General Settings - Line ending format (newlines)
jQuery('#input_select_newlines').change(function (event) {
if (jQuery('#input_select_newlines').val() == 'windows') {
newline = '\r\n' // windows
localStorage.setItem('newline', 'windows')
} else {
newline = '\n' // unix
localStorage.setItem('newline', 'unix')
}
console.log(jQuery('#input_select_newlines').val() + ' line endings selected')
})
// General Settings - Night mode
jQuery('#input_checkbox_night_mode').change(function (event) {
if (jQuery('#input_checkbox_night_mode').is(':checked')) {
jQuery('body').addClass('dark')
localStorage.setItem('night_mode', true)
} else {
jQuery('body').removeClass('dark')
localStorage.setItem('night_mode', false)
}
})
// Synth Settings - Waveform
jQuery('#input_select_synth_waveform').change(function (event) {
synth.waveform = jQuery('#input_select_synth_waveform').val()
update_page_url()
})
// Synth Settings - Amplitude Envelope
jQuery('#input_select_synth_amp_env').change(function (event) {
update_page_url()
})
// Synth Settings - Delay
jQuery('#input_checkbox_delay_on').change(function () {
if (jQuery(this).is(':checked')) {
synth.delay.enable()
} else {
synth.delay.disable()
}
})
jQuery(document).on('input', '#input_range_feedback_gain', function () {
synth.delay.gain = jQuery(this).val()
console.log(synth.delay.gain)
const now = synth.now()
synth.delay.gainL.gain.setValueAtTime(synth.delay.gain, now)
synth.delay.gainR.gain.setValueAtTime(synth.delay.gain, now)
})
jQuery(document).on('change', '#input_range_delay_time', function () {
synth.delay.time = jQuery(this).val() * 0.001
const now = synth.now()
synth.delay.channelL.delayTime.setValueAtTime(synth.delay.time, now)
synth.delay.channelR.delayTime.setValueAtTime(synth.delay.time, now)
})
jQuery(document).on('input', '#input_range_delay_time', function () {
jQuery('#delay_time_ms').text(jQuery(this).val())
})
// Synth Settings - Max polyphony
jQuery('#input_number_max_polyphony').change(function (event) {
let value = jQuery('#input_number_max_polyphony').val()
// only save if input is not empty
if (!R.isEmpty(value)) {
localStorage.setItem('max_polyphony', value)
}
})
// Isomorphic Settings - Keyboard Layout
jQuery('#input_select_keyboard_layout').change(function (event) {
var id = jQuery('#input_select_keyboard_layout').val()
synth.keymap = Keymap[id]
localStorage.setItem('keybd_region', id)
})
// Isomorphic Settings - Isomorphic Mapping
jQuery('#input_number_isomorphicmapping_vert').change(function (event) {
synth.isomorphicMapping.vertical = jQuery('#input_number_isomorphicmapping_vert').val()
})
jQuery('#input_number_isomorphicmapping_horiz').change(function (event) {
synth.isomorphicMapping.horizontal = jQuery('#input_number_isomorphicmapping_horiz').val()
})
// Isomorphic Settings - Key colors
jQuery('#input_key_colors').change(function (event) {
set_key_colors(jQuery('#input_key_colors').val())
// update this change in the browser's Back/Forward navigation
update_page_url()
})
// initialise key colors. defaults to Halberstadt layout on A
set_key_colors(jQuery('#input_key_colors').val())
// Isomorphic Settings - Key colors Auto button clicked
jQuery('#btn_key_colors_auto').click(function (event) {
event.preventDefault()
var size = tuning_table['note_count'] - 1
var colors = ''
// fall back in some situations
if (size < 2) {
if (R.isEmpty(jQuery('#input_key_colors').val())) {
// field is empty so we'll apply a sensible default key colouring
jQuery('#input_key_colors').val(
'white black white white black white black white white black white black'
)
set_key_colors(jQuery('#input_key_colors').val())
return true
}
// field already has content so we'll do nothing
return false
}
switch (size.toString()) {
case '9':
colors = 'white white black black white white black black white'
break
case '10':
colors = 'white black white white white black white white black white'
break
case '11':
colors = 'white black white black white black white black white black white'
break
case '12':
colors = 'white black white white black white black white white black white black'
break
case '13':
colors =
'antiquewhite white black white black white white black white white black white black'
break
case '14':
colors =
'white black white black white black white white black white black white black white'
break
case '15':
colors =
'white black white black white black white black white black white black white black white'
break
case '16':
colors =
'white black white black white black white white black white black white black white black white'
break
case '17':
colors =
'white black black white white black black white black black white white black black white black black'
break
case '18':
colors =
'white black black white black white black black white black white black black white black black white black'
break
case '19':
colors =
'white black grey white black grey white black white black grey white black grey white black grey white black white'
break
case '20':
colors =
'white white black black white white black black white white black black white white black black white white black black'
break
case '21':
colors =
'white black black white black black white black black white black black white black black white black black white black black'
break
case '22':
colors =
'white black white black white black white black white black white white black white black white black white black white black white'
break
case '23':
colors =
'white black black black white black black white black black white black black white black black white black black black white black black black'
break
case '24':
colors =
'white lightgrey black dimgrey white lightgrey white lightgrey black dimgrey white lightgrey black dimgrey white lightgrey white lightgrey black dimgrey white lightgrey black dimgrey'
break
default:
{
// assemble a key colouring for any arbitrary scale size
let sequenceOfColors = []
for (let i = 0; i < Math.floor(size / 2); i++) {
sequenceOfColors.push('white', 'black')
}
if (size % 2 === 1) {
sequenceOfColors.push('white')
}
colors = sequenceOfColors.join(' ')
}
break
}
jQuery('#input_key_colors').val(colors)
set_key_colors(colors)
// update this change in the browser's Back/Forward navigation
update_page_url()
return true
})
// Social Icons
// Email
jQuery('a.social-icons-email').click(function (event) {
event.preventDefault()
var email = ''
var subject = encodeURIComponent('Scale Workshop - ' + jQuery('#txt_name').val())
var emailBody = encodeURIComponent(
'Sending you this musical scale:' +
newline +
jQuery('#txt_name').val() +
newline +
newline +
'The link below has more info:' +
newline +
newline +
jQuery('#input_share_url').val()
)
window.location = 'mailto:' + email + '?subject=' + subject + '&body=' + emailBody
})
// Twitter
jQuery('a.social-icons-twitter').click(function (event) {
event.preventDefault()
var text = encodeURIComponent(jQuery('#txt_name').val() + ' ♫ ')
var url = encodeURIComponent(jQuery('#input_share_url').val())
window.open('https://twitter.com/intent/tweet?text=' + text + url)
})
// parse tuning data when changes are made
jQuery(
'#txt_name, #txt_tuning_data, #txt_base_frequency, #txt_base_midi_note, #input_select_newlines'
).change(function () {
parse_tuning_data()
})
// handle QWERTY key active indicator
is_qwerty_active()
jQuery('input,textarea')
.focusin(() => {
is_qwerty_active()
})
.focusout(() => {
is_qwerty_active()
})
// Remove splash screen
jQuery('div#splash').fadeOut()
// now everything is initialised we finally run any custom user scripts
run_user_scripts_on_document_ready()
const onModeChange = (input) => {
const cntr = $('#input_modify_mode_counter')
const updateCntr = (value) => {
cntr.text(value)
}
const isInputEmpty = input === ''
const isInputValid = /^(\d+\s+)*\d+$/.test(input)
let sum = 0
if (!isInputEmpty && isInputValid) {
const numbers = input.split(/\s+/).map((numberString) => parseInt(numberString))
sum = R.sum(numbers)
}
updateCntr(sum)
}
$('#input_modify_mode').on('input', (e) => {
onModeChange(e.target.value.trim())
})
onModeChange($('#input_modify_mode').val().trim())
}) // end of document ready block
================================================
FILE: src/js/exporters.js
================================================
function export_error() {
// no tuning data to export
if (R.isNil(tuning_table['freq'][tuning_table['base_midi_note']])) {
alert('No tuning data to export.')
return true
}
}
function save_file(filename, contents, raw, mimeType = 'application/octet-stream,') {
const link = document.createElement('a')
link.download = filename
if (raw === true) {
const blob = new Blob([contents], { type: 'application/octet-stream' })
link.href = window.URL.createObjectURL(blob)
} else {
link.href = 'data:' + mimeType + encodeURIComponent(contents)
}
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })) // opens save dialog
}
function export_anamark_tun(version) {
if (export_error()) {
return
}
// TUN format spec:
// http://www.mark-henning.de/files/am/Tuning_File_V2_Doc.pdf
if (version === undefined) {
version = 100
}
// Assemble the .tun file contents:
// Comment section
var file = '; VAZ Plus/AnaMark softsynth tuning file' + newline
file += '; ' + jQuery('#txt_name').val() + newline
file += ';' + newline
var scale_url = get_scale_url()
// If version 200 or higher, display the scale URL so user can easily get back to the original scale that generates this tun file.
// If earlier than version 200, we must be careful that a long URL doesn't break the line-length limit of 512 characters.
// Note: TUN spec says line-length limit is 255 but in the v1 file format source the limit is indeed 512.
if (version >= 200 || scale_url.length <= 508) {
file += '; ' + scale_url + newline
}
// If version before 200 and URL is too long, fall back to an alternative way of displaying the original scale data.
else {
for (i = 1; i < tuning_table.scale_data.length; i++) {
file += '; ' + tuning_table.scale_data[i] + newline
}
}
file += ';' + newline
file += '; VAZ Plus section' + newline
file += '[Tuning]' + newline
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
file +=
'note ' +
i +
'=' +
parseInt(decimal_to_cents(parseFloat(tuning_table['freq'][i]) / mtof(0))) +
newline
}
file += newline + '; AnaMark section' + newline
file += '[Scale Begin]' + newline
file += 'Format= "AnaMark-TUN"' + newline
file += 'FormatVersion= ' + version + newline
file += 'FormatSpecs= "http://www.mark-henning.de/eternity/tuningspecs.html"' + newline + newline
file += '[Info]' + newline
file += 'Name= "' + tuning_table['filename'] + '.tun"' + newline
file += 'ID= "' + tuning_table['filename'].replace(/ /g, '') + '.tun"' + newline // this line strips whitespace from filename, as per .tun spec
file += 'Filename= "' + tuning_table['filename'] + '.tun"' + newline
file += 'Description= "' + tuning_table['description'] + '"' + newline
var date = new Date().toISOString().slice(0, 10)
file += 'Date= "' + date + '"' + newline
file += 'Editor= "' + APP_TITLE + '"' + newline + newline
file += '[Exact Tuning]' + newline
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
file +=
'note ' +
i +
'= ' +
decimal_to_cents(parseFloat(tuning_table['freq'][i]) / mtof(0)).toFixed(6) +
newline
}
// version 2.00 only
if (version >= 200) {
file += newline + '[Functional Tuning]' + newline
for (let i = 1; i < tuning_table['note_count']; i++) {
if (i == tuning_table['note_count'] - 1) {
file +=
'note ' +
i +
'="#>-' +
i +
' % ' +
decimal_to_cents(tuning_table['tuning_data'][i]).toFixed(6) +
' ~999"' +
newline
} else {
file +=
'note ' +
i +
'="#=0 % ' +
decimal_to_cents(tuning_table['tuning_data'][i]).toFixed(6) +
'"' +
newline
}
}
file +=
newline + '; Set reference key to absolute frequency (not scale note but midi key)' + newline
file +=
'note ' +
tuning_table['base_midi_note'] +
'="! ' +
tuning_table['base_frequency'].toFixed(6) +
'"' +
newline
}
file += newline + '[Scale End]' + newline
save_file(tuning_table['filename'] + '.tun', file)
// success
return true
}
function export_scala_scl() {
if (export_error()) {
return
}
// assemble the .scl file contents
var file = '! ' + tuning_table['filename'] + '.scl' + newline
file += '! Created using ' + APP_TITLE + newline
file += '!' + newline
file += '! ' + get_scale_url() + newline
file += '!' + newline
if (R.isEmpty(jQuery('#txt_name').val())) {
file += 'Untitled tuning'
} else {
file += jQuery('#txt_name').val()
}
file += newline + ' '
file += tuning_table['note_count'] - 1 + newline
file += '!' + newline
for (let i = 1; i < tuning_table['note_count']; i++) {
file += ' '
// if the current interval is n-of-m edo or commadecimal linetype, output as cents instead
if (
getLineType(tuning_table['scale_data'][i]) === LINE_TYPE.N_OF_EDO ||
getLineType(tuning_table['scale_data'][i]) === LINE_TYPE.DECIMAL ||
( // Don't allow ratio integers above (2^31) - 1
getLineType(tuning_table['scale_data'][i]) === LINE_TYPE.RATIO &&
tuning_table['scale_data'][i].split('/').reduce((overflow, d) => overflow || parseInt(d) > 2147483647, false)
)
) {
file += decimal_to_cents(tuning_table['tuning_data'][i]).toFixed(6)
} else {
file += tuning_table['scale_data'][i]
}
file += newline
}
save_file(tuning_table['filename'] + '.scl', file)
// success
return true
}
function export_scala_kbm() {
if (export_error()) {
return
}
// assemble the .kbm file contents
var file = '! Template for a keyboard mapping' + newline
file += '!' + newline
file += '! Size of map. The pattern repeats every so many keys:' + newline
file += parseInt(tuning_table['note_count'] - 1) + newline
file += '! First MIDI note number to retune:' + newline
file += '0' + newline
file += '! Last MIDI note number to retune:' + newline
file += '127' + newline
file += '! Middle note where the first entry of the mapping is mapped to:' + newline
file += parseInt(tuning_table['base_midi_note']) + newline
file += '! Reference note for which frequency is given:' + newline
file += parseInt(tuning_table['base_midi_note']) + newline
file += '! Frequency to tune the above note to' + newline
file += parseFloat(tuning_table['base_frequency']) + newline
file += '! Scale degree to consider as formal octave (determines difference in pitch' + newline
file += '! between adjacent mapping patterns):' + newline
file += parseInt(tuning_table['note_count'] - 1) + newline
file += '! Mapping.' + newline
file += '! The numbers represent scale degrees mapped to keys. The first entry is for' + newline
file += '! the given middle note, the next for subsequent higher keys.' + newline
file +=
'! For an unmapped key, put in an "x". At the end, unmapped keys may be left out.' + newline
for (let i = 0; i < parseInt(tuning_table['note_count'] - 1); i++) {
file += i + newline
}
save_file(tuning_table['filename'] + '.kbm', file)
// success
return true
}
function export_maxmsp_coll() {
if (export_error()) {
return
}
// assemble the coll file contents
var file = '# Tuning file for Max/MSP coll objects. - Created using ' + APP_TITLE + newline
file += '# ' + jQuery('#txt_name').val() + newline
file += '#' + newline
file += '# ' + get_scale_url() + newline
file += '#' + newline
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
file += i + ', ' + tuning_table['freq'][i].toFixed(7) + ';' + newline
}
save_file(tuning_table['filename'] + '.txt', file)
// success
return true
}
function export_pd_text() {
if (export_error()) {
return
}
// assemble the text file contents
var file = ''
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
file += tuning_table['freq'][i].toFixed(7) + ';' + newline
}
save_file(tuning_table['filename'] + '.txt', file)
// success
return true
}
function export_kontakt_script() {
if (export_error()) {
return
}
// assemble the kontakt script contents
var file = '{**************************************' + newline
file += jQuery('#txt_name').val() + newline
file +=
'MIDI note ' +
tuning_table['base_midi_note'] +
' (' +
midi_note_number_to_name(tuning_table['base_midi_note']) +
') = ' +
parseFloat(tuning_table['base_frequency']) +
' Hz' +
newline
file += 'Created using ' + APP_TITLE + newline + newline
file += get_scale_url() + newline
file += '****************************************}' + newline + newline
file += 'on init' + newline
file += 'declare %keynum[' + TUNING_MAX_SIZE + ']' + newline
file += 'declare %tune[' + TUNING_MAX_SIZE + ']' + newline
file += 'declare $bend' + newline
file += 'declare $key' + newline + newline
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
var this_note = ftom(tuning_table['freq'][i])
// if we're out of range of the default Kontakt tuning, leave note as default tuning
if (this_note[0] < 0 || this_note[0] >= TUNING_MAX_SIZE) {
file += '%keynum[' + i + '] := ' + i + newline
file += '%tune[' + i + '] := 0' + newline
}
// success, we're in range of another note, so we'll change the tuning +/- 50c
else {
file += '%keynum[' + i + '] := ' + this_note[0] + newline
file += '%tune[' + i + '] := ' + parseInt(this_note[1] * 1000) + newline
}
}
file += 'end on' + newline + newline
file += 'on note' + newline
file += '$key := %keynum[$EVENT_NOTE]' + newline
file += '$bend := %tune[$EVENT_NOTE]' + newline
file += 'change_note ($EVENT_ID, $key)' + newline
file += 'change_tune ($EVENT_ID, $bend, 0)' + newline
file += 'end on' + newline
save_file(tuning_table['filename'] + '.txt', file)
// success
return true
}
function export_soniccouture_nka() {
if (export_error()) {
return
}
// assemble the nka contents
// first line should always be "%XenSetup"
var file = '%XenSetup' + newline
// loop through 128 notes to get semitone offset
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
var this_note = ftom(tuning_table['freq'][i])
// if we're out of MIDI note range, leave semitone offset as default
if (this_note[0] < 0 || this_note[0] >= TUNING_MAX_SIZE) {
file += '0' + newline
}
// success, we're in range of another note, so get the semitone offset
else {
file += this_note[0] - i + newline
}
}
// loop through 128 notes to get cents offset
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
var this_note = ftom(tuning_table['freq'][i])
// if we're out of MIDI note range, leave semitone offset as default
if (this_note[0] < 0 || this_note[0] >= TUNING_MAX_SIZE) {
file += '0' + newline
}
// success, we're in range of another note, so we'll change the tuning +/- 5000 hundredths of a cent
else {
file += parseInt(this_note[1] * 100) + newline
}
}
// Soniccouture .nka format requires 0 followed by newline to end the file
file += '0' + newline
save_file(tuning_table['filename'] + '.nka', file)
// success
return true
}
function exportImageLinePitchMap(range) {
const { clamp } = R
if (export_error()) {
return
}
const NB_NOTES = 121 // IL products can only retune from C0 to C10
const HEADER_BYTES = Uint8Array.from([3, 0, 0, 0, 3, 0, 0, 0, NB_NOTES, 0, 0, 0])
const ENDING_BYTES = Uint8Array.from([
0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
])
const X_STRIDE = 1 / NB_NOTES // constant x offset from one point to the next
const CURVE_DATA = 33554432 // curve data for straight line, observed experimentally
const baseFreqOffset = Math.log2(tuning_table.base_frequency / 440) // in number of octaves
// construct point data
let points = new ArrayBuffer(121 * 24)
let pointsDoubles = new Float64Array(points)
let pointsUint32 = new Uint32Array(points)
for (let i = 0; i < NB_NOTES; i++) {
const edo12cents = (i - 69) * 100
const offset = tuning_table.cents[i] - edo12cents
const normalizedOffset = ((offset / 1200 + baseFreqOffset) / range) * 0.5 + 0.5
const yCoord = clamp(0, 1, normalizedOffset)
pointsDoubles[i * 3 + 1] = yCoord
if (i !== 0) {
// no x offset and no curve data for first point
pointsDoubles[i * 3] = X_STRIDE
pointsUint32[i * 6 + 4] = 0
pointsUint32[i * 6 + 5] = CURVE_DATA
}
}
// assemble .fnv file
let file = new Uint8Array(HEADER_BYTES.length + points.byteLength + ENDING_BYTES.length)
let offset = 0
file.set(HEADER_BYTES, offset)
offset += HEADER_BYTES.length
file.set(new Uint8Array(points), offset)
offset += points.byteLength
file.set(ENDING_BYTES, offset)
save_file(tuning_table.filename + '.fnv', file, true)
// success
return true
}
function exportHarmorPitchMap() {
exportImageLinePitchMap(5)
}
function exportSytrusPitchMap() {
exportImageLinePitchMap(4)
}
function getMnlgtunTuningInfoXML(useScaleFormat, programmer, comment) {
// Builds an XML file necessary for the .mnlgtun file format
const rootName = useScaleFormat
? 'minilogue_TuneScaleInformation'
: 'minilogue_TuneOctInformation'
const xml = document.implementation.createDocument(null, rootName)
const Programmer = xml.createElement('Programmer')
Programmer.textContent = programmer
xml.documentElement.appendChild(Programmer)
const Comment = xml.createElement('Comment')
Comment.textContent = comment
xml.documentElement.appendChild(Comment)
return xml
}
function getMnlgtunFileInfoXML(useScaleFormat, product = 'minilogue') {
// Builds an XML file necessary for the .mnlgtun file format
const rootName = 'KorgMSLibrarian_Data'
const xml = document.implementation.createDocument(null, rootName)
const Product = xml.createElement('Product')
Product.textContent = product
xml.documentElement.appendChild(Product)
const Contents = xml.createElement('Contents')
Contents.setAttribute('NumProgramData', 0)
Contents.setAttribute('NumPresetInformation', 0)
Contents.setAttribute('NumTuneScaleData', 1 * useScaleFormat)
Contents.setAttribute('NumTuneOctData', 1 * !useScaleFormat)
const [fileNameHeader, dataName, binName] = useScaleFormat
? ['TunS_000.TunS_', 'TuneScaleData', 'TuneScaleBinary']
: ['TunO_000.TunO_', 'TuneOctData', 'TuneOctBinary']
const TuneData = xml.createElement(dataName)
const Information = xml.createElement('Information')
Information.textContent = fileNameHeader + 'info'
TuneData.appendChild(Information)
const BinData = xml.createElement(binName)
BinData.textContent = fileNameHeader + 'bin'
TuneData.appendChild(BinData)
Contents.appendChild(TuneData)
xml.documentElement.appendChild(Contents)
return xml
}
function exportMnlgtun(useScaleFormat) {
// This exporter converts tuning data into a zip-compressed file for use with Korg's
// 'logue Sound Librarian software, supporting their 'logue series of synthesizers.
// While this exporter preserves accuracy as much as possible, the Sound Librarian software
// unforunately truncates cent values to 1 cent precision. It's unknown whether the tuning accuracy
// from this exporter is written to the synthesizer and used in the synthesis.
if (export_error()) {
return
}
// the index of the table that's equal to the baseNote should have the following value
const refOffsetCents =
MNLG_A_REF.val + decimal_to_cents(tuning_table.base_frequency / MNLG_A_REF.freq)
// offset cents array for binary conversion
let centsTable = tuning_table.cents.map((c) => roundToNDecimals(3, c + refOffsetCents))
if (useScaleFormat) {
// ensure table length is exactly 128
centsTable = centsTable.slice(0, MNLG_SCALESIZE)
// this shouldn't happen unless something goes really wrong
if (centsTable.length !== MNLG_SCALESIZE) {
console.log(
'Somehow the mnlgtun table was less than 128 values, the end will be padded with 0s.'
)
const padding = new Array(MNLG_SCALESIZE - centsTable.length).fill(0)
centsTable = [...centsTable, ...padding]
}
} else {
// normalize around root, truncate to 12 notes, and wrap flattened Cs
let cNote = parseInt(tuning_table.base_midi_note / MNLG_OCTAVESIZE) * MNLG_OCTAVESIZE
centsTable = centsTable
.slice(cNote, cNote + MNLG_OCTAVESIZE)
.map((cents) => mathModulo(cents - MNLG_C_REF.val, MNLG_MAXCENTS))
}
// convert to binary
const binaryData = centsTableToMnlgBinary(centsTable)
// prepare files for zipping
const tuningInfo = getMnlgtunTuningInfoXML(useScaleFormat, 'ScaleWorkshop', tuning_table.filename)
const fileInfo = getMnlgtunFileInfoXML(useScaleFormat)
const [fileNameHeader, fileType] = useScaleFormat
? ['TunS_000.TunS_', '.mnlgtuns']
: ['TunO_000.TunO_', '.mnlgtuno']
// build zip
const zip = new JSZip()
zip.file(fileNameHeader + 'bin', binaryData)
zip.file(fileNameHeader + 'info', tuningInfo.documentElement.outerHTML)
zip.file('FileInformation.xml', fileInfo.documentElement.outerHTML)
zip.generateAsync({ type: 'base64' }).then(
(base64) => {
save_file(tuning_table.filename + fileType, base64, false, 'application/zip;base64,')
},
(err) => alert(err)
)
// success
return true
}
function export_reference_deflemask() {
// This exporter converts your tuning data into a readable format you can easily input manually into Deflemask.
// For example if you have a note 50 cents below A4, you would input that into Deflemask as A-4 -- - E5 40
// Deflemask manual: http://www.deflemask.com/manual.pdf
if (export_error()) {
return
}
// assemble the text file contents
var file =
tuning_table['description'] +
newline +
'Reference for Deflemask note input - generated by ' +
APP_TITLE +
newline +
newline
file += get_scale_url() + newline + newline
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
// convert frequency into midi note number + cents offset
var data = ftom(tuning_table['freq'][i])
// acceptable range is C#0 to B7 (MIDI notes 1-95). skip this note if it's out of range
if (data[0] < 1 || data[0] > 95) continue
// convert note number to note name
data[0] = midi_note_number_to_name(data[0])
data[0] = data[0].length == 2 ? data[0].slice(0, 1) + '-' + data[0].slice(1) : data[0]
// convert cents offset to hex where -100c=00, 0c=80, 100c=FF
data[1] = Math.round(128 + data[1] * 1.28)
.toString(16)
.toUpperCase()
// add data to text file
data = '[' + data[0] + ' xx] [xx E5 ' + data[1] + ']'
file +=
data +
' ..... ' +
i +
': ' +
tuning_table['freq'][i].toFixed(2) +
' Hz / ' +
tuning_table['cents'][i].toFixed(2) +
' cents' +
newline
}
save_file(tuning_table['filename'] + '.txt', file)
// success
return true
}
// TODO: improve with currying?
function exportReaperNamedNotes(
pitchFormat = 'scale data',
showPeriodNumbers = true,
calculatePeriodInPitch = false,
rootPeriod = 0,
centsRoot = 0,
degreeRoot = 0
) {
// This exporter enumerates the scale data to 128 MIDI notes in a readable format
// that can be loaded into Reaper's piano roll in "Named Note" mode.
// - 'pitchFormat' can either be 'scale data', 'cents', 'freq', 'decimal', or 'degree'
// - 'showPeriodNumbers' if true will put the period (or octave) number by each pitch
// - 'calculatePeriodInPitch' if true will preserve the period value in the pitch value
// - 'rootPeriod' is for if the root should start on a certain period number
// - 'centsRoot' is the cent value used for the root note of the scale
if (export_error()) {
return false
}
// Prepare suffix containing chosen options
let options = ['NoteNames']
if (pitchFormat !== 'scale data') options.push([pitchFormat])
if (showPeriodNumbers) {
if (rootPeriod !== 0) {
const rootPeriodStr = rootPeriod < 0 ? rootPeriod : `+${rootPeriod}`
options.push(`${rootPeriodStr}p`)
} else options.push('p')
}
if (pitchFormat === 'cents' && centsRoot !== 0) {
const centsRootStr = centsRoot < 0 ? centsRoot : `+${centsRoot}`
options.push(`${centsRootStr}c`)
} else if (pitchFormat === 'degree' && degreeRoot !== 0) {
const degreeRootStr = degreeRoot < 0 ? degreeRoot : `+${degreeRoot}`
options.push(`${degreeRootStr}deg`)
}
if (calculatePeriodInPitch) options.push('exact')
const filenameSuffix = options.join('_')
// general properties
const tuningSize = tuning_table.note_count - 1
const period = tuning_table.scale_data[tuningSize]
// line building functions
const prepend = (num, line) => `${num} ${line}`
const rootOffset = (num) => num - tuning_table.base_midi_note
const circularIndex = (num) => mathModulo(rootOffset(num), tuningSize)
const periodNumber = (num) => Math.floor(rootOffset(num) / tuningSize + rootPeriod)
const appendPeriodNum = (line, num) => `${line} (${periodNumber(num)})`
const calcPeriod = (line, ind) =>
transposeLine(line, transposeSelf(period, periodNumber(ind) + rootPeriod))
const addCentsRoot = (cents) => parseFloat(cents) + centsRoot
let fileFunction, pitchTable
// start file
let file = '# MIDI note / CC name map' + newline
let pitchLine = (line, ind) => line
if (showPeriodNumbers) pitchLine = (line, ind) => appendPeriodNum(line, ind)
if (pitchFormat === 'scale data') {
const unison = transposeSelf(period, 0) // use a 1/1 in the line type of the period
pitchTable = [unison, ...tuning_table.scale_data.slice(1, -1)]
let scalePitch = calculatePeriodInPitch
? (line, ind) => pitchLine(calcPeriod(line, ind), ind)
: (line, ind) => pitchLine(line, ind)
// Iterate over scale data, applying periods if chosen
const scaleData = (num, array, table) => {
const ind = array.length - num - 1
return prepend(ind, scalePitch(table[circularIndex(ind)], ind))
}
fileFunction = (table) =>
tuning_table.cents.map((x, i, a) => scaleData(i, a, table)).join(newline)
} else if (pitchFormat !== 'degree') {
let pitchOffset = (line, ind) => pitchLine(roundToNDecimals(6, parseFloat(line)), ind)
// assign proper pitch table
// will have 6 decimal places of precision, except for cents which is 3
switch (pitchFormat) {
case 'freq':
pitchTable = tuning_table.freq
break
case 'decimal':
pitchTable = tuning_table.decimal
break
default:
pitchTable = tuning_table.cents
pitchOffset = (line, ind) => pitchLine(roundToNDecimals(3, addCentsRoot(line)), ind)
break
}
// iterate over the first period of the table
if (!calculatePeriodInPitch) {
const pitchValue = (i, table) => {
const ind = table.length - i - 1
return prepend(
ind,
pitchOffset(table[circularIndex(ind) + tuning_table.base_midi_note], ind)
)
}
fileFunction = (table) => table.map((x, i, a) => pitchValue(i, a)).join(newline)
// iterate over the whole table
} else {
const pitchValue = (i, table) => {
const ind = table.length - i - 1
return prepend(ind, pitchOffset(table[ind], ind))
}
fileFunction = (table) => table.map((x, i) => pitchValue(i, table)).join(newline)
}
// enumerate scale degrees
} else {
pitchTable = tuning_table.cents
const degreeLine = (num, table) => {
const ind = table.length - num - 1
let deg = rootOffset(ind) + degreeRoot + tuningSize * rootPeriod
if (!calculatePeriodInPitch) deg = mathModulo(deg, tuningSize)
return prepend(ind, pitchLine(`${deg}\\${tuningSize}`, ind))
}
fileFunction = (table) => table.map((x, i) => degreeLine(i, table)).join(newline)
}
file += fileFunction(pitchTable)
save_file(`${tuning_table.filename} ${filenameSuffix}.txt`, file)
// success
return true
}
/**
* get_export_url()
*/
function get_scale_url() {
var url = new URL(window.location.href)
var protocol = !R.isEmpty(url.protocol) ? url.protocol + '//' : 'http://'
var host = url.host
var pathname = !R.isEmpty(url.pathname) ? url.pathname : '/scaleworkshop/'
// var domain = !R.isNil(window.location.href) ? window.location.href : 'http://sevish.com/scaleworkshop';
var name = encodeURIComponent(jQuery('#txt_name').val())
var data = encodeURIComponent(jQuery('#txt_tuning_data').val())
var freq = encodeURIComponent(jQuery('#txt_base_frequency').val())
var midi = encodeURIComponent(jQuery('#txt_base_midi_note').val())
var vert = encodeURIComponent(synth.isomorphicMapping.vertical)
var horiz = encodeURIComponent(synth.isomorphicMapping.horizontal)
var colors = encodeURIComponent(jQuery('#input_key_colors').val())
var waveform = encodeURIComponent(jQuery('#input_select_synth_waveform').val())
var ampenv = encodeURIComponent(jQuery('#input_select_synth_amp_env').val())
return (
protocol +
host +
pathname +
'?name=' +
name +
'&data=' +
data +
'&freq=' +
freq +
'&midi=' +
midi +
'&vert=' +
vert +
'&horiz=' +
horiz +
'&colors=' +
colors +
'&waveform=' +
waveform +
'&env=' +
ampenv
)
}
/**
* update_page_url()
*/
function update_page_url(url = get_scale_url()) {
// update this change in the browser's Back/Forward navigation
history.pushState({}, tuning_table['description'], url)
}
/**
* export_url()
*/
function export_url() {
var export_url = window.location.href
if (export_error()) {
export_url = 'http://sevish.com/scaleworkshop/'
}
// copy url in to url field
jQuery('#input_share_url').val(export_url)
console.log('export_url = ' + export_url)
jQuery('#input_share_url').select()
jQuery('#modal_share_url').dialog({
modal: true,
buttons: {
'Copy URL': function () {
jQuery('#input_share_url').select()
document.execCommand('Copy')
jQuery(this).dialog('close')
}
}
})
// url field clicked
jQuery('#input_share_url').click(function (event) {
jQuery(this).select()
})
// success
return true
}
================================================
FILE: src/js/generators.js
================================================
/**
* TUNING DATA GENERATORS
*/
function generate_equal_temperament() {
var divider = getFloat('#input_number_of_divisions', 'Warning: no divider')
var period = getString('#input_interval_to_divide', 'Warning: no interval to divide')
// convert period to cents
var period_cents = line_to_cents(period);
// bail if period is invalid
if (period_cents === false) {
return false;
}
setScaleName(divider + " equal divisions of " + period)
setTuningData(generate_equal_temperament_data(divider, parseFloat(period_cents)));
parse_tuning_data();
closePopup('#modal_generate_equal_temperament')
// success
return true;
}
function generate_equal_temperament_data(divider, period) {
// calculate the size of a single step in this tuning
var step = period / divider;
let notes = []
for (i = 1; i <= divider; i++) {
var note = roundToNDecimals(6, i * step);
// if returned value is an integer, append a . just to make sure the parser will see it as a cents value later
if (!note.toString().includes('.')) {
note = note.toString() + ".";
}
notes.push(note)
}
return notes.join(unix_newline)
}
function generate_rank_2_temperament() {
var generator = getLine('#input_rank-2_generator', 'Warning: no generator')
var generatorType = getLineType(generator);
// bail if generator is invalid
if (generatorType === LINE_TYPE.INVALID) {
return false;
}
var period = getLine('#input_rank-2_period', 'Warning: no period')
var periodType = getLineType(period);
// bail if period is invalid
if (periodType === LINE_TYPE.INVALID) {
return false;
}
var size = parseInt(jQuery("#input_rank-2_size").val());
var up = parseInt(jQuery("#input_rank-2_up").val());
if (isNaN(size) || size < 2) {
alert('Warning: scale size must be a number greater than 1');
return false;
}
if (isNaN(up) || up < 0 || up >= size) {
alert('Warning: generators up must be a number greater than -1 and less than the scale size');
return false;
}
var lineType = jQuery("#input_rank-2_type").val();
setTuningData(generate_rank_2_temperament_data(generator, period, size, up, lineType))
setScaleName("Rank 2 scale (" + generator + ", " + period + ")");
parse_tuning_data();
closePopup('#modal_generate_rank_2_temperament');
// success
return true;
}
function generate_rank_2_temperament_data(generator, period, size, up, lineType) {
// empty existing tuning data
let tuningData = '';
// I added these to fix a NaN issue, but they might have been a result of a typo.
// If there's still NaN with some params, try uncommenting out these if-statements.
// Should only affect ginormous integer ratios - vsicurella
// Convert to cents if ratio integers are longer than 20 digits
// if (getLineType(generator) === LINE_TYPE.RATIO && !ratioIsSafe(generator))
// generator = ratio_to_cents(generator);
// if (getLineType(period) === LINE_TYPE.RATIO && !ratioIsSafe(period))
// period = ratio_to_cents(period);
// Scale down generator to start within the period
// (helps avoid NaN and 0.0 cents with huge integer ratios)
generator = moduloLine(generator, period);
// Start scale on the lowest generator
let powers = up - size + 1;
let scale = [moduloLine(transposeSelf(generator, powers), period)];
// Transpose each line by the generator and period reduce
for (let i = 1; i < size; i++)
scale.push(moduloLine(transposeSelf(generator, ++powers), period));
// sort the scale ascending
scale.sort((a, b) => [a, b].map(line_to_decimal).reduce((a, b) => a - b));
// add the period to the scale
scale.push(period);
// convert scale to output line type
switch (lineType) {
case "cents":
scale = scale.map(x => line_to_cents(x).toFixed(6));
break;
case "decimals":
scale = scale.map(x => line_to_commadecimal(x, 6));
}
tuningData += scale.slice(1, size + 1).join(unix_newline);
return tuningData;
}
function generate_harmonic_series_segment() {
var lo = getFloat('#input_lowest_harmonic', 'Warning: lowest harmonic should be a positive integer')
var hi = getFloat('#input_highest_harmonic', 'Warning: highest harmonic should be a positive integer')
// bail if lo = hi
if (lo == hi) {
alert("Warning: Lowest and highest harmonics are the same. Can't generate a scale based on only one harmonic.");
return false;
}
// ensure that lo is lower than hi
if (lo > hi) {
[lo, hi] = [hi, lo]
}
setScaleName("Harmonics " + lo + "-" + hi);
setTuningData(generate_harmonic_series_segment_data(lo, hi));
parse_tuning_data();
closePopup("#modal_generate_harmonic_series_segment");
// success
return true;
}
function generate_harmonic_series_segment_data(lo, hi) {
let ratios = []
for (i = lo + 1; i <= hi; i++) {
// add ratio to text box
ratios.push(i + "/" + lo)
}
return ratios.join(unix_newline)
}
function generate_subharmonic_series_segment() {
var lo = getFloat('#input_lowest_subharmonic', 'Warning: lowest subharmonic should be a positive integer')
var hi = getFloat('#input_highest_subharmonic', 'Warning: highest subharmonic should be a positive integer')
// bail if lo = hi
if (lo == hi) {
alert("Warning: Lowest and highest subharmonics are the same. Can't generate a scale based on only one subharmonic.");
return false;
}
// ensure that lo is lower than hi
if (lo > hi) {
[lo, hi] = [hi, lo]
}
setTuningData(generate_subharmonic_series_segment_data(lo, hi));
setScaleName("Subharmonics " + lo + "-" + hi);
parse_tuning_data();
closePopup("#modal_generate_subharmonic_series_segment");
// success
return true;
}
function generate_subharmonic_series_segment_data(lo, hi) {
let ratios = []
for (i = hi - 1; i >= lo; i--) {
ratios.push(hi + "/" + i)
}
return ratios.join(unix_newline)
}
function generate_enumerate_chord() {
var chord = getString('#input_chord', 'Warning: bad input');
let chordStr = chord;
var convert_to_ratios = document.getElementById("input_convert_to_ratios").checked;
// It doesn't make much sense to mix different values,
// but it's cool to experiment with.
// bail if has invalid
var inputTest = chord.replace(" ", "").replace("(", "").replace(")", "").split(":");
if (inputTest.length < 2) {
alert("Warning: Chord needs more than one pitch of the form A:B:C...");
return false;
}
for (var i = 0; i < inputTest.length; i++) {
var eval = inputTest[i];
if (/^\d+$/.test(eval))
eval += ",";
eval = line_to_decimal(eval);
if (eval == 0 || !/(^\d+([\,\.]\d*)?|([\\\/]\d+)?$)*/.test(eval)) {
alert("Warning: Invalid pitch " + inputTest[i])
return false;
}
}
// check if it's a tonal inversion
// ex: 1/(A:B:C...)
var isInversion = document.getElementById("input_invert_chord").checked;
if (isInversion)
chordStr = "1/(" + chord + ")";
if (/^\d+\/\(.*$/.test(chord)) {
if (/^1\/\((\d+\:)+\d+\)$/.test(chord)) {
isInversion = true;
chord = chord.substring(3, chord.length - 1);
} else {
alert("Warning: inversions need to match this syntax: 1/(A:B:C...)");
return false;
}
}
// This next safeguard might make it more user friendy,
// but I think it's a bit limiting for certain purposes a more advanced
// user might try like using NOfEdo values to build chords.
// bail if first note is in cents
//if (isCent(pitches[0]) || isNOfEdo(pitches[0])) {
// alert("Warning: first pitch cannot be in cents");
// return false;
//}
if (isInversion) {
console.log("This is an inversion. Chord is " + chord);
chord = invert_chord(chord);
console.log("Chord returned: " + chord);
chordStr += (" (" + chord + ") ");
console.log("str = " + chordStr);
}
var pitches = chord.split(":");
// TODO: if pitches are not harmonics but "convert_to_ratios" is true,
// update name with proper harmonics format
setScaleName("Chord " + chordStr);
setTuningData(generate_enumerate_chord_data(pitches, convert_to_ratios));
parse_tuning_data();
closePopup("#modal_enumerate_chord");
// success
return true;
}
function generate_enumerate_chord_data(pitches, convertToRatios = false) {
let ratios = [];
var fundamental = 1;
for (var i = 0; i < pitches.length; i++) {
// convert a lone integer to a commadecimal
if (/^\d+$/.test(pitches[i])) {
pitches[i] = pitches[i] + ',';
}
var isCentsValue = isCent(pitches[i]) || isNOfEdo(pitches[i]);
var parsed = line_to_decimal(pitches[i]);
if (i > 0) {
if (isCentsValue && !convertToRatios) {
ratios.push(pitches[i])
} else {
ratios.push(decimal_to_ratio(parsed / fundamental));
}
}
else {
fundamental = parsed;
}
}
return ratios.join(unix_newline)
}
function generate_cps() {
// get factors and combination count
var f = getString('#input_cps_factors', 'Warning: no factors');
var cc = getFloat('#input_cps_combination_count', 'Warning: combination count should be minimum of 2 and less than the number of factors.');
// bail on missing input
if (f === false || cc === false) {
return false;
}
// convert input factors to array
var factors = f.split(" ");
// cc must be integer. discard anything after decimal point
cc = parseInt(cc);
// bail on invalid combination count
if ( cc < 2 || cc >= factors.length ) {
alert("Combination count should be minimum of 2 and less than the number of factors.");
return false;
}
// loop through the array of factors
for (let i=0; i < factors.length; i++) {
// only allow integers - these will treated as harmonics
factors[i] = parseInt(factors[i]);
// future improvement - floats to be allowed. non-integers to be treated as decimals or cents?
// factors[i] = parseFloat(factors[i]);
// bail if any of the factors aren't a number
if ( isNaN(factors[i]) ) {
alert("Factors should be a list of integers e.g. '1 3 7 9'");
return false;
}
}
// do combinations
var products = cps(factors,cc);
// remove 1/1 from scale by dividing all products by one of the factors
if (document.getElementById("input_cps_remove_1").checked) {
// remove first product from the set
var divisor = products.shift();
// divide remaining products by the removed product
for (let i=0; i 0) {
let equave = tuning_table.tuning_data[tuning_table.note_count - 1]
for (i = 0; i < tuning_table.note_count; i++) {
let pos = 1 + (w - 2) * (Math.log(tuning_table.tuning_data[i]) / Math.log(equave))
ctx.beginPath()
ctx.moveTo(pos, h * 0.4)
ctx.lineTo(pos, h * 0.6)
ctx.strokeStyle = '#555'
ctx.lineWidth = 3
ctx.stroke()
}
}
}
// init graphics
render_graphic_scale_rule()
================================================
FILE: src/js/helpers.js
================================================
/**
* HELPER FUNCTIONS
*/
// Set precision to cover possible integers before scientific notation
Decimal.precision = 21
// modulo function
Number.prototype.mod = function (n) {
return ((this % n) + n) % n
}
// modulo function (forward compatibility)
function mathModulo(n, d) {
return ((n % d) + d) % d
}
// logarithm-based modulo function, only supporting modulus > 1
function logModulo(n, d) {
const [floatn, floatd] = [n, d].map(parseFloat);
if ( floatn === 0
|| floatd <= 1
|| Number.isNaN(floatn)
|| Number.isNaN(floatd)
)
return NaN;
const [nlog, dlog] = [Decimal.log2(n), Decimal.log2(d)];
const modPower = nlog.div(dlog).floor();
return Decimal(n).div(Decimal(d).pow(modPower)).toNumber();
}
// convert a cents value to decimal
function cents_to_decimal(input) {
const inputfloat = parseFloat(input);
if (Number.isNaN(inputfloat)) return NaN;
return Decimal.pow(2, Decimal(input).div(1200)).toNumber();
}
// convert a ratio (string 'x/y') to decimal
function ratio_to_decimal(input) {
if (isRatio(input)) {
const [val1, val2] = input.split('/').map(Decimal);
return val1.div(val2).toNumber();
} else {
alert('Invalid input: ' + input)
return false
}
}
// convert a comma decimal (1,25) to decimal
function commadecimal_to_decimal(input) {
if (isCommaDecimal(input)) {
input = parseFloat(input.toString().replace(',', '.'))
if (input === 0 || isNaN(input)) {
return false
} else {
return input
}
} else {
alert('Invalid input: ' + input)
return false
}
}
// convert a decimal (1.25) into commadecimal (1,25)
function decimal_to_commadecimal(input) {
if (/^\d+\.?\d*$/.test(input)) {
return input.toFixed(6).replace('.', ',')
} else {
alert('Invalid input: ' + input)
return false
}
}
// convert a decimal into cents
function decimal_to_cents(input) {
if (input === false) {
return false
}
const inputfloat = parseFloat(input);
if (Number.isNaN(inputfloat) || inputfloat === 0) {
return false
} else {
input = Decimal(input)
return Decimal.log2(input).mul(1200).toNumber();
}
}
// convert a ratio to cents
function ratio_to_cents(input) {
return decimal_to_cents(ratio_to_decimal(input))
}
// convert an n-of-m-edo (string 'x\y') to decimal
function n_of_edo_to_decimal(input) {
if (isNOfEdo(input)) {
const [val1, val2] = input.split('\\').map(Decimal)
return Decimal(2).pow(val1.div(val2)).toNumber();
} else {
alert('Invalid input: ' + input)
return false
}
}
// convert an n-of-m-edo (string 'x\y') to cents
function n_of_edo_to_cents(input) {
return decimal_to_cents(n_of_edo_to_decimal(input))
}
function isCent(input) {
// true, when the input has numbers at the beginning, followed by a dot, ending with any number of numbers
// for example: 700.00, -700.00
if (typeof input !== 'string') {
return false
}
return /^-?\d+\.\d*$/.test(input.trim())
}
function isCommaDecimal(input) {
// true, when the input has numbers at the beginning, followed by a comma, ending with any number of numbers
// for example: 1,25
if (typeof input !== 'string') {
return false
}
return /^\d+\,\d*$/.test(input.trim())
}
function isNOfEdo(input) {
// true, when the input has numbers at the beginning and the end, separated by a single backslash
// for example: 7\12, -7\12
return /^-?\d+\\\d+$/.test(input)
}
function isRatio(input) {
// true, when the input has numbers at the beginning and the end, separated by a single slash
// for example: 3/2
return /^\d+\/\d+$/.test(input)
}
function getLineType(input) {
if (isCent(input)) {
return LINE_TYPE.CENTS
}
if (isCommaDecimal(input)) {
return LINE_TYPE.DECIMAL
}
if (isNOfEdo(input)) {
return LINE_TYPE.N_OF_EDO
}
if (isRatio(input)) {
return LINE_TYPE.RATIO
}
return LINE_TYPE.INVALID
}
// convert any input 'line' to decimal
function line_to_decimal(input) {
let converterFn = () => false
switch (getLineType(input)) {
case LINE_TYPE.CENTS:
converterFn = cents_to_decimal
break
case LINE_TYPE.DECIMAL:
converterFn = commadecimal_to_decimal
break
case LINE_TYPE.N_OF_EDO:
converterFn = n_of_edo_to_decimal
break
case LINE_TYPE.RATIO:
converterFn = ratio_to_decimal
break
}
return converterFn(input)
}
// convert any input 'line' to commadecimal, with a padding options for display
function line_to_commadecimal(input, padDecimals = 0, truncateDecimalsPastPad = false) {
let decimal = line_to_decimal(input)
if (decimal === false) return decimal
let decimalStr = String(decimal)
// Padding stuff
if (padDecimals > 0) {
if (decimalStr.includes('.')) {
const decLength = decimalStr.split('.')[1].length
if (padDecimals > decLength)
for (var i = 0; i < padDecimals - decLength; i++) decimalStr += '0'
else if (truncateDecimalsPastPad && decLength > padDecimals)
decimalStr = decimalStr.slice(0, decimalStr.indexOf('.') + padDecimals + 1)
} else decimalStr += '.000000'
}
decimalStr = decimalStr.replace('.', ',')
return decimalStr
}
function isNegativeInterval(input) {
// true if cents or N of EDO evaluates to a negative number
// LINE_TYPE.INVALID if invalid type or if ratio, decimal,
// or N of EDO denominator is negative
// false otherwise
if (typeof input !== 'string') return LINE_TYPE.INVALID
const hasNegation = input.match('-') !== null;
const type = getLineType(input);
switch(type) {
// Zero is nonnegative
case LINE_TYPE.CENTS:
return (/^0+\.0*$/.test(input)) ? false : hasNegation;
case LINE_TYPE.N_OF_EDO:
return (input.trim()[0] === '0') ? false : hasNegation;
case LINE_TYPE.RATIO:
case LINE_TYPE.DECIMAL:
return (hasNegation) ? LINE_TYPE.INVALID : false;
default:
return LINE_TYPE.INVALID
}
}
// convert any input 'line' to a cents value
function line_to_cents(input) {
return decimal_to_cents(line_to_decimal(input))
}
// convert a midi note number to a frequency in Hertz
// assuming 12-edo at 440Hz
function mtof(input) {
const frequencyOfC0 = 8.17579891564
return frequencyOfC0 * Math.pow(SEMITONE_RATIO_IN_12_EDO, parseInt(input))
}
// convert a frequency to a midi note number and cents offset
// assuming 12-edo at 440Hz
// returns an array [midi_note_number, cents_offset]
function ftom(input) {
const midiNoteNumberOfA4 = 69
var midi_note_number = midiNoteNumberOfA4 + 12 * Math.log2(parseFloat(input) / 440)
var cents_offset = (midi_note_number - Math.round(midi_note_number)) * 100
midi_note_number = Math.round(midi_note_number)
return [midi_note_number, cents_offset]
}
// convert an input string into a filename-sanitized version
// if input is empty, returns "tuning" as a fallback
function sanitize_filename(input) {
if (R.isEmpty(input.trim())) {
return 'untitled scale'
}
return input.replace(/[|&;$%@"<>()+,?]/g, '').replace(/\//g, '_')
}
// clear all inputted scale data
function clear_all() {
const midiNoteNumberOfA4 = 69
// empty text fields
jQuery('#txt_tuning_data').val('')
jQuery('#txt_name').val('')
// empty any information displayed on page
jQuery('#tuning-table').empty()
// restore default base tuning
jQuery('#txt_base_frequency').val(440)
jQuery('#txt_base_midi_note').val(midiNoteNumberOfA4)
// re-init tuning_table
tuning_table = {
scale_data: [], // an array containing list of intervals input by the user
tuning_data: [], // an array containing the same list above converted to decimal format
note_count: 0, // number of values stored in tuning_data
freq: [], // an array containing the frequency for each MIDI note
cents: [], // an array containing the cents value for each MIDI note
decimal: [], // an array containing the frequency ratio expressed as decimal for each MIDI note
base_frequency: 440, // init val
base_midi_note: midiNoteNumberOfA4, // init val
description: '',
filename: ''
}
// re-draw graphics
render_graphic_scale_rule()
}
// find MIDI note name from MIDI note number
function midi_note_number_to_name(input) {
var n = parseInt(input)
var quotient = Math.floor(n / 12)
var remainder = n % 12
var name = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
return name[remainder] + quotient
}
// calculate the sum of the values in a given array given a stopping index
function sum_array(array, endIndex) {
return array.slice(0, endIndex).reduce((sum, x) => sum + parseInt(x), 0)
}
// rotates the array by given steps
function rotate(array, steps) {
let startInd = array.length - mathModulo(steps, array.length)
return [...array.slice(startInd), ...array.slice(0, startInd)]
}
// calculate a continued fraction for the given number
function get_cf(num, maxiterations = 15, roundf = 10) {
const numfloat = parseFloat(num);
if (numfloat === 0 || maxiterations < 1) return [0]
else if (Number.isNaN(numfloat)) return NaN
num = Decimal(num)
var cf = [] // the continued fraction
var digit
var roundInv = Decimal(0.1).pow(roundf);
var iterations = 0
while (iterations < maxiterations) {
digit = num.floor().toNumber();
cf.push(digit)
num = num.sub(digit)
if (num.eq(0) || num.lte(roundInv))
break
num = Decimal(1).div(num);
iterations++
}
return cf
}
// calculate a single convergent for a given continued fraction
function get_convergent(cf, depth = 0) {
// Return whole number if cf is a number
if (typeof cf === 'number') {
let cfNum = parseInt(cf)
if (cfNum === 0) return '0/1'
else if (!cfNum) return NaN
else return `${cfNum}/1`
}
// Make sure indicies are valid
let parsedCf = []
for (let num of cf) {
num = parseInt(num)
if (isNaN(num)) return NaN
parsedCf.push(num)
}
var cfdigit // the continued fraction digit
var num // the convergent numerator
var den // the convergent denominator
var tmp // for easy reciprocation
if (depth >= parsedCf.length || depth == 0) depth = parsedCf.length
for (var d = 0; d < depth; d++) {
cfdigit = parsedCf[d]
num = cfdigit
den = 1
// calculate the convergent
for (var i = d; i > 0; i--) {
tmp = den
den = num
num = tmp
num += den * parsedCf[i - 1]
}
}
return `${num}/${den}`
}
// convert a decimal or commadecimal to ratio (string 'x/y'), may have rounding errors for irrationals
function decimal_to_ratio(input, iterations = 15, depth = 0) {
if (isCommaDecimal(input)) input = commadecimal_to_decimal(input)
if (input === false) return false
const inputfloat = parseFloat(input)
if (inputfloat === 0 || Number.isNaN(inputfloat)) {
return false
} else {
var inputcf = get_cf(input, iterations, 6)
return get_convergent(inputcf, depth)
}
}
function cents_to_ratio(input, iterations = 15, depth = 0) {
return decimal_to_ratio(cents_to_decimal(input), iterations, depth)
}
function n_of_edo_to_ratio(input, iterations = 15, depth = 0) {
return decimal_to_ratio(n_of_edo_to_decimal(input), iterations, depth)
}
// calculate all best rational approximations given a continued fraction
function get_convergents(cf, numarray, denarray, perlimit, cindOut = null) {
var cfdigit // the continued fraction digit
var num // the convergent numerator
var den // the convergent denominator
var scnum // the semiconvergent numerator
var scden // the semiconvergen denominator
var cind = [] // tracks indicies of convergents
for (var d = 0; d < cf.length; d++) {
cfdigit = cf[d]
num = cfdigit
den = 1
// calculate the convergent
for (var i = d; i > 0; i--) {
;[den, num] = [num, den]
num += den * cf[i - 1]
}
if (d > 0) {
for (var i = 1; i < cfdigit; i++) {
scnum = num - (cfdigit - i) * numarray[cind[d - 1]]
scden = den - (cfdigit - i) * denarray[cind[d - 1]]
if (scden > perlimit) break
numarray.push(scnum)
denarray.push(scden)
}
}
if (den > perlimit) break
cind.push(numarray.length)
numarray.push(num)
denarray.push(den)
}
if (!(cindOut === null)) {
for (var i = 0; i < cind.length; i++) {
cindOut.push(cind[i])
}
}
//for (var i = 0; i < denarray.length; i++)
// console.log(numarray[i]+"/"+denarray[i]);
}
// generate and display MOS list
function show_mos_cf(per, gen, ssz, threshold) {
var maxsize = 400 // maximum period size
var maxcfsize = 12 // maximum continued fraction length
var roundf = 4 // rounding factor in case continued fraction blows up
threshold = Decimal(threshold);
per = line_to_decimal(per)
if (per <= 0 || isNaN(per)) {
jQuery('#info_rank_2_mos').text('invalid period')
return false
}
gen = line_to_decimal(gen)
if (gen <= 0 || isNaN(gen)) {
jQuery('#info_rank_2_mos').text('invalid generator')
return false
}
var genlog = Decimal.log(gen).div(Decimal.log(per)).toNumber(); // the logarithmic ratio to generate MOS info
var cf = [] // continued fraction
var nn = [] // MOS generators
var dd = [] // MOS periods
cf = get_cf(genlog, maxcfsize, roundf)
get_convergents(cf, nn, dd, maxsize)
// filter by step size threshold
var gc = decimal_to_cents(gen)
var pc = decimal_to_cents(per)
var L = pc + gc // Large step
var s = pc // small step
var c = gc // chroma (L - s)
let roundInv = Decimal(1).div(roundf);
for (var i = 1; i < cf.length; i++) {
L -= c * cf[i]
s = c
c = L - s
// break if g is some equal division of period
if (roundInv.gte(c) && cf.length < maxcfsize) {
// add size-1
// not sure if flaw in the algorithm or weird edge case
if (dd[dd.length - 2] != dd[dd.length - 1] - 1)
dd.splice(dd.length - 1, 0, dd[dd.length - 1] - 1)
break
}
if (threshold.gte(c)) {
var ind = sum_array(cf, i + 1)
dd.splice(ind + 1, dd.length - ind)
break
}
}
// the first two periods are trivial
dd.shift()
dd.shift()
jQuery('#info_rank_2_mos').text(dd.join(', '))
}
// helper function to simply pass in an interval and get an array of ratios returned
function get_rational_approximations(
intervalIn,
numerators,
denominators,
roundf = 999999,
cidxOut = null,
ratiosOut = null,
numlimits = null,
denlimits = null,
ratiolimits = null
) {
var cf = [] // continued fraction
cf = get_cf(intervalIn, 15, roundf)
get_convergents(cf, numerators, denominators, roundf, cidxOut)
var doRatios = !(ratiosOut === null)
var doNumLim = !(numlimits === null)
var doDenLim = !(denlimits === null)
var doRatioLim = !(ratiolimits === null)
if (doRatios || doNumLim || doDenLim || doRatioLim) {
var nlim
var dlim
var rlim
for (var i = 0; i < numerators.length; i++) {
numerators[i] == 1 ? (nlim = 1) : (nlim = get_prime_limit(numerators[i]))
denominators[i] == 1 ? (dlim = 1) : (dlim = get_prime_limit(denominators[i]))
if (doRatios) ratiosOut.push(numerators[i] + '/' + denominators[i])
if (doNumLim) numlimits.push(nlim)
if (doDenLim) denlimits.push(dlim)
if (doRatioLim) ratiolimits.push(Math.max(nlim, dlim))
}
}
}
// rank2 scale algorithm intended for integers, in ET contexts
// for example, period = 12, gen = 7 : [ 2 2 1 2 2 2 1 ]
function get_rank2_mode(period, generator, size, numdown = 0) {
let degrees = []
let modeOut = []
var interval
interval = generator * -numdown
for (var n = 0; n < size; n++) {
while (interval < 0) {
interval += period
}
if (interval >= period) {
interval %= period
}
degrees.push(interval)
interval += generator
}
degrees.sort(function (a, b) {
return a - b
})
for (var n = 1; n < degrees.length; n++) {
modeOut.push(degrees[n] - degrees[n - 1])
}
modeOut.push(period - degrees[degrees.length - 1])
return modeOut
}
// returns an array representing the prime factorization
// indicies are the 'nth' prime, the value is the powers of each prime
function get_prime_factors(number) {
number = Math.floor(number)
if (number == 1) {
//alert("Warning: 1 has no prime factorization.");
return 1
}
var factorsout = []
var n = number
var q = number
var loop
for (var i = 0; i < PRIMES.length; i++) {
if (PRIMES[i] > n) break
factorsout.push(0)
if (PRIMES[i] == n) {
factorsout[i]++
break
}
loop = true
while (loop) {
q = n / PRIMES[i]
if (q == Math.floor(q)) {
n = q
factorsout[i]++
continue
}
loop = false
}
}
return factorsout
}
function get_prime_factors_string(number) {
var factors = get_prime_factors(number)
var str_out = ''
for (var i = 0; i < factors.length; i++) {
if (factors[i] != 0) {
str_out += PRIMES[i] + '^' + factors[i]
if (i < factors.length - 1) str_out += ' * '
}
}
return str_out
}
function isPrime(number) {
var sqrtnum = Math.floor(Math.sqrt(number))
for (var i = 0; i < PRIMES.length; i++) {
if (PRIMES[i] >= sqrtnum) break
if (number % PRIMES[i] == 0) {
return false
}
}
return true
}
function prevPrime(number) {
if (number < 2) return 2
var i = 0
while (i < PRIMES.length && PRIMES[i++] <= number);
return PRIMES[i - 2]
}
function nextPrime(number) {
if (number < 2) return 2
var i = 0
while (i < PRIMES.length && PRIMES[i++] <= number);
return PRIMES[i - 1]
}
function closestPrime(number) {
var thisPrime = isPrime(number)
if (number < 2) return 2
else if (thisPrime) return number
var np = nextPrime(number)
var pp = prevPrime(number)
if (Math.abs(np - number) < Math.abs(pp - number)) return np
else return pp
}
function scrollToPrime(number, scrollDown) {
if (scrollDown) return prevPrime(number)
else return nextPrime(number)
}
function get_prime_limit(number) {
var factors = get_prime_factors(number)
return PRIMES[factors.length - 1]
}
function get_prime_limit_of_ratio(numerator, denominator) {
return Math.max(get_prime_limit(numerator), get_prime_limit(denominator))
}
// returns an array of integers that share no common factors to the given integer
function get_coprimes(number) {
let coprimes = [1]
var m, d, t
for (var i = 2; i < number - 1; i++) {
m = number
d = i
while (d > 1) {
m = m % d
t = d
d = m
m = t
}
if (d > 0) {
coprimes.push(i)
}
}
coprimes.push(number - 1)
return coprimes
}
// returns an array of integers that can divide evenly into given number
function get_factors(number) {
let factors = []
var nsqrt = Math.floor(Math.sqrt(number))
for (var n = 2; n <= nsqrt; n++) {
var q = number / n
if (Math.floor(q) == q) {
factors.push(n)
if (n != q) factors.push(q)
}
}
return factors.sort(function (a, b) {
return a - b
})
}
function getGCD(num1, num2) {
const floats = [num1, num2].map(parseFloat);
for (const float of floats) if (!Number.isInteger(float) || Number.isNaN(float)) return NaN;
const n1 = Decimal.abs(num1);
const n2 = Decimal.abs(num2);
if (n1.eq(0) || n2.eq(0))
return n1.add(n2).valueOf();
if (n1.eq(1) || n2.eq(1))
return "1";
if (n1.eq(n2))
return n1.valueOf();
return getGCD(n2.valueOf(), n1.mod(n2).valueOf());
}
// TODO: GCD of an array
function getLCM(num1, num2) {
if (num1 === 0 || num2 === 0) return 0
const gcd = getGCD(num1, num2)
return Math.trunc((Math.max(num1, num2) / gcd) * Math.min(num1, num2))
}
function getLCMArray(array) {
let primecounts = []
let primefactors = []
var f
array.forEach(function (item, index, array) {
f = get_prime_factors(item)
primefactors.push(f)
})
var maxlength = 0
primefactors.forEach(function (item, index, array) {
if (item.length > maxlength) maxlength = item.length
})
// find the min power of each primes in numbers' factorization
for (var p = 0; p < maxlength; p++) {
primecounts.push(0)
for (var n = 0; n < primefactors.length; n++) {
f = primefactors[n]
if (p < f.length) {
if (primecounts[p] < f[p]) primecounts[p] = f[p]
}
}
}
let lcm = 1
primecounts.forEach(function (item, index) {
lcm *= Math.pow(PRIMES[index], item)
})
return lcm
}
// returns false if a ratio divides by 0 or contains NaN
function ratioIsValid(ratio) {
if (typeof ratio !== 'string')
return false;
// allow negatives
if (isNegativeInterval(ratio))
ratio = Array.from(ratio).filter(char => char !== '-').join('');
if (getLineType(ratio) !== LINE_TYPE.RATIO)
return false;
const [num, den] = ratio.split('/').map(parseFloat);
if (Number.isNaN(num) || Number.isNaN(den) || den === 0)
return false;
return true;
}
// returns false is a ratio contains an integer with more than 20 digits
function ratioIsSafe(ratio) {
const [num, den] = ratio.split('/').map(Decimal);
if (num.e > 20 || den.e > 20)
return false;
return true;
}
// returns a reduced form of given ratio
// returns NaN if the ratio is invalid or unsafe
function simplifyRatio(ratio) {
if (!ratioIsValid(ratio) || !ratioIsSafe(ratio))
return NaN;
const [numerator, denominator] = ratio.split('/');
const gcd = getGCD(numerator, denominator);
if (gcd == 0 || Number.isNaN(parseFloat(gcd)))
return NaN;
const gcdScalar = Decimal(1).div(gcd);
let numSigned = Decimal(numerator).mul(Decimal.sign(denominator));
let denAbs = Decimal.abs(denominator);
const [numOut, denOut] = [numSigned, denAbs].map(x => x.mul(gcdScalar).round());
return `${numOut}/${denOut}`;
}
function transposeRatios(ratio, transposerRatio) {
if (!ratioIsValid(ratio) || !ratioIsValid(transposerRatio))
return NaN;
let bailToCents = () => `${roundToNDecimals(6, ratio_to_cents(ratio) + ratio_to_cents(transposerRatio))}`;
if (!ratioIsSafe(ratio) || !ratioIsSafe(transposerRatio))
return bailToCents();
const [n1, d1] = ratio.split('/').map(Decimal);
const [n2, d2] = transposerRatio.split('/').map(Decimal);
// TODO simplification in place
const numProduct = n1.mul(n2).valueOf();
const denProduct = d1.mul(d2).valueOf();
const product = `${numProduct}/${denProduct}`;
if (!ratioIsSafe(product))
return bailToCents();
return simplifyRatio(product);
}
function powRatio(ratio, power) {
if (!ratioIsValid(ratio) || Number.isNaN(parseFloat(power)))
return NaN;
power = Decimal(power);
const bailToCents = () => `${roundToNDecimals(6, power.mul(line_to_cents(ratio)).valueOf())}`;
if (!ratioIsSafe(ratio) || !ratioIsSafe(`${power}/1`))
return bailToCents();
const simplified = simplifyRatio(ratio);
if (Number.isNaN(simplified))
return NaN;
if (!ratioIsSafe(simplified))
return bailToCents();
let ratioStrings = simplified.split('/');
if (Decimal.sign(power) < 0)
ratioStrings = ratioStrings.reverse();
const result = ratioStrings.map(x => Decimal.pow(x, power.abs())).join('/');
if (!ratioIsSafe(result))
return bailToCents();
return result;
}
// Return a ratio between 1 and the period, where the period cannot be less than 1
function periodReduceRatio(ratio, period) {
if (!ratioIsValid(ratio) || !ratioIsValid(period))
return NaN;
const [ratioNum, ratioDen] = ratio.split('/').map(Decimal);
const ratioDecimal = Decimal(ratioNum).div(ratioDen);
const [periodNum, periodDen] = period.split('/').map(Decimal);
const periodDecimal = Decimal(periodNum).div(periodDen);
if (periodDecimal.lt(1))
return NaN;
// See if period is a perfect root of ratio
const root = ratioDecimal.ln().div(periodDecimal.ln());
if (root.isInteger())
{
const reducedNum = ratioNum.div(periodNum.pow(root)).round();
const reducedDen = ratioDen.div(periodDen.pow(root)).round();
return `${reducedNum}/${reducedDen}`;
}
const bailToCents = () => `${roundToNDecimals(6, mathModulo(line_to_cents(ratio), line_to_cents(period)))}`;
if (!ratioIsSafe(ratio) || !ratioIsSafe(period))
return bailToCents();
const power = ratioDecimal.ln().div(periodDecimal.ln()).floor();
// Make scalars from both numerator and denominator of the period ratio to keep integers
// The reciprocal of the period ratio is used, and if the 'power' is negative, the scalars are also reciprocated
const periodNumPower = Decimal.pow(periodDen, power.abs());
const periodDenPower = Decimal.pow(periodNum, power.abs());
const periodNumScalar = (Decimal.sign(power) > 0) ? [ periodNumPower, 1 ] : [ 1, periodNumPower ];
const periodDenScalar = (Decimal.sign(power) > 0) ? [ periodDenPower, 1 ] : [ 1, periodDenPower ];
// Cross multiply with period's numerator & denominator scalars
const ratioReducedNum = Decimal(ratioNum).mul(periodNumScalar[0]).mul(periodDenScalar[1]);
const ratioReducedDen = Decimal(ratioDen).mul(periodNumScalar[1]).mul(periodDenScalar[0]);
// TODO simplify in place
const result = `${ratioReducedNum}/${ratioReducedDen}`;
if (!ratioIsSafe(result))
return bailToCents();
return simplifyRatio(result);
}
function transposeNOfEdos(nOfEdo, transposerNOfEdo) {
if (typeof nOfEdo !== 'string' || typeof transposerNOfEdo !== 'string') return NaN
const [deg1, edo1] = nOfEdo.split('\\').map((x) => parseInt(x))
const [deg2, edo2] = transposerNOfEdo.split('\\').map((x) => parseInt(x))
if (!edo1 || !edo2 || isNaN(deg1) || isNaN(deg2)) return NaN
const newEdo = getLCM(edo1, edo2)
const newDegree = (newEdo / edo1) * deg1 + (newEdo / edo2) * deg2
return [newDegree, newEdo].join('\\')
}
// transpose an interval by another interval,
// retaining their types when possible
function transposeLine(line, transposer) {
const lineType = getLineType(line);
const transposerType = getLineType(transposer);
if (lineType === LINE_TYPE.INVALID || transposerType === LINE_TYPE.INVALID)
return NaN;
// If both are ratios, preserve ratio notation
if (lineType === LINE_TYPE.RATIO) {
if (transposerType === LINE_TYPE.RATIO) return transposeRatios(line, transposer)
else if (transposerType === LINE_TYPE.DECIMAL) {
let ratio2 = decimal_to_ratio(transposer);
return transposeRatios(line, ratio2);
}
// see if cents or N of EDO is an octave
else {
let octs = Decimal.log2(line_to_decimal(transposer));
if (octs.isInteger()) { // TODO - work with other harmonics?
const octRatio = Decimal.pow(2, octs.abs());
const octTransposer = (octs.lt(0)) ? "1/" + octRatio
: octRatio + "/1";
return transposeRatios(line, octTransposer);
}
}
} else if (lineType === LINE_TYPE.N_OF_EDO) {
// If both are N of EDOs, preserve N of EDO notation
if (transposerType === LINE_TYPE.N_OF_EDO) return transposeNOfEdos(line, transposer)
// See if second type is a power of two
const line2Ratio = roundToNDecimals(6, line_to_decimal(transposer));
let octs = Decimal.log2(line2Ratio);
if (octs.isInteger())
return transposeNOfEdos(line, `${octs}\\1`);
// Return result as commadecimal type
if (transposerType === LINE_TYPE.DECIMAL)
return decimal_to_commadecimal(n_of_edo_to_decimal(line) * line2Ratio)
}
// If the first line is a decimal type, keep decimals
else if (lineType === LINE_TYPE.DECIMAL) {
const lineDecimal = line_to_decimal(line);
let transposerDecimal = line_to_decimal(transposer);
return decimal_to_commadecimal(lineDecimal * transposerDecimal);
}
// All other cases convert to cents, allow negative values
let lineCents = line_to_cents(line);
let transposerCents = line_to_cents(transposer);
const valueOut = lineCents + transposerCents
return roundToNDecimals(6, valueOut).toFixed(6)
}
// stacks an interval on itself, like a power function.
// if transposeAmt=0, this returns unison.
// if transposeAmt=1, this returns the line unchanged.
function transposeSelf(line, transposeAmt) {
const lineType = getLineType(line);
const lineIsNegative = isNegativeInterval(line);
if (lineIsNegative === LINE_TYPE.INVALID || lineType === LINE_TYPE.INVALID || typeof transposeAmt !== "number")
return NaN;
const wholeExp = Number.isInteger(transposeAmt);
// power function
if (lineType === LINE_TYPE.DECIMAL)
return decimal_to_commadecimal(Math.pow(line_to_decimal(line), transposeAmt))
else if (wholeExp && lineType === LINE_TYPE.RATIO) {
return powRatio(line, transposeAmt);
}
// multiply degree by transpose amount
else if (wholeExp && lineType === LINE_TYPE.N_OF_EDO) {
let [deg, edo] = line.split("\\");
deg *= transposeAmt;
return `${deg}\\${edo}`;
}
let value = transposeAmt * line_to_cents(line);
return value.toFixed(6);
}
function moduloLine(line, modLine) {
const modType = getLineType(modLine);
const modIsNegative = isNegativeInterval(modLine);
if (modType === LINE_TYPE.INVALID || modIsNegative)
return NaN;
const lineIsNegative = isNegativeInterval(line);
const lineType = getLineType(line);
if (lineIsNegative === LINE_TYPE.INVALID || lineType === LINE_TYPE.INVALID)
return NaN;
if (lineType !== LINE_TYPE.CENTS) {
// Preserve N of EDO notation
if (lineType === LINE_TYPE.N_OF_EDO) {
let [numDeg, numEdo] = line.split("\\").map((x) => parseInt(x));
// If both are N of EDOs, get LCM edo
if (modType === LINE_TYPE.N_OF_EDO) {
const [modDeg, modEdo] = modLine.split('\\').map((x) => parseInt(x))
const lcmEdo = getLCM(numEdo, modEdo)
return `${((numDeg * lcmEdo) / numEdo) % ((modDeg * lcmEdo) / modEdo)}\\${lcmEdo}`
}
// See if mod is a power of 2
const modDecimal = line_to_decimal(modLine)
const modLog2 = Decimal.log2(modDecimal)
if (modLog2.isInteger()) {
return `${mathModulo(numDeg, numEdo)}\\${numEdo}`
}
}
// Preserve ratio type if possible
if (lineType === LINE_TYPE.RATIO) {
if (modType === LINE_TYPE.RATIO) {
const modDecimal = line_to_decimal(modLine);
if (modDecimal < 1) return NaN;
return periodReduceRatio(line, modLine)
}
// See if mod type is a reasonable whole number ratio
const modDecimal = line_to_decimal(modLine);
if (modDecimal < 1) return NaN;
const mod_cf = get_cf(modDecimal);
if (mod_cf.length < 12) {
const modRatio = get_convergent(mod_cf);
return periodReduceRatio(line, modRatio);
}
}
// Preserve decimal type
else if (lineType === LINE_TYPE.DECIMAL || modType === LINE_TYPE.DECIMAL) {
return decimal_to_commadecimal([line, modLine].map(line_to_decimal).reduce(logModulo))
}
}
// All other cases convert to cents
const [lineCents, modCents] = [line, modLine].map(x => roundToNDecimals(12, line_to_cents(x)));
const centsMod = mathModulo(lineCents, modCents);
return roundToNDecimals(6, centsMod).toFixed(6);
}
// inverts a line into its negative form, while preserving line-type
function negateLine(line) {
switch (getLineType(line)) {
case LINE_TYPE.RATIO:
let [num, den] = line.split('/')
return den + '/' + num
case LINE_TYPE.DECIMAL:
return decimal_to_commadecimal(1 / commadecimal_to_decimal(line))
case LINE_TYPE.CENTS:
if (!isNegativeInterval(line)) {
return '-' + line
} else {
return line.replace('-', '')
}
case LINE_TYPE.N_OF_EDO:
if (!isNegativeInterval(line)) {
return '-' + line
} else {
return line.replace('-', '')
}
default:
return NaN
}
}
function invert_chord(chord) {
if (!/^(\d+:)+\d+$/.test(chord)) {
alert('Warning: invalid chord ' + chord)
return false
}
let reduced = chord.split(':').map((x) => parseInt(x))
let interavls = []
reduced.forEach(function (item, index, array) {
if (index > 0) {
interavls.push([item, array[index - 1]])
}
})
interavls.reverse()
reduced = [[1, 1]]
let denominators = []
interavls.forEach((x, index) => {
const num = Decimal(x[0]).mul(reduced[index][0]);
const den = Decimal(x[1]).mul(reduced[index][1]);
const ratio = `${num}/${den}`
const simplified = simplifyRatio(ratio);
if (Number.isNaN(simplified))
return;
const [n, d] = simplified.split('/').map(x => Decimal(x).valueOf());
reduced.push([n, d]);
denominators.push(parseInt(d.valueOf()));
})
var lcm = getLCMArray(denominators);
chord = []
reduced.forEach(function (x) {
chord.push(Decimal(x[0]).mul(lcm).div(x[1]).valueOf());
})
return chord.join(':');
}
const roundToNDecimals = (decimals, number) => {
return Math.round(number * 10 ** decimals) / 10 ** decimals
}
const findIndexClosestTo = (value, array) => {
return array.map((x) => Math.abs(value - x)).reduce((ci, d, i, a) => (d < a[ci] ? i : ci), 0)
}
function getFloat(id, errorMessage) {
var value = parseFloat(jQuery(id).val())
if (isNaN(value) || value === 0) {
alert(errorMessage)
return false
}
return value
}
function getString(id, errorMessage) {
var value = jQuery(id).val()
if (R.isEmpty(value) || R.isNil(value)) {
alert(errorMessage)
return false
}
return value
}
function getLine(id, errorMessage) {
var value = jQuery(id).val()
if (
R.isEmpty(value) ||
parseFloat(value) <= 0 ||
R.isNil(value) ||
getLineType(value) === LINE_TYPE.INVALID
) {
alert(errorMessage)
return false
}
return value
}
function setScaleName(title) {
jQuery('#txt_name').val(title)
}
function closePopup(id) {
jQuery(id).dialog('close')
}
function setTuningData(tuning) {
jQuery('#txt_tuning_data').val(tuning)
}
const isFunction = (x) => typeof x === 'function'
function getCoordsFromKey(tdOfKeyboard) {
try {
return JSON.parse(tdOfKeyboard.getAttribute('data-coord'))
} catch (e) {
return []
}
}
function getSearchParamOr(valueIfMissing, key, url) {
return url.searchParams.has(key) ? url.searchParams.get(key) : valueIfMissing
}
function getSearchParamAsNumberOr(valueIfMissingOrNan, key, url) {
return url.searchParams.has(key) && !isNaN(url.searchParams.get(key))
? parseFloat(url.searchParams.get(key))
: valueIfMissingOrNan
}
function trimSelf(el) {
jQuery(el).val(function (idx, val) {
return val.trim()
})
}
function openDialog(el, onOK) {
jQuery(el).dialog({
modal: true,
buttons: {
OK: onOK,
Cancel: function () {
jQuery(this).dialog('close')
}
}
})
}
// redirect all traffic to https, if not there already
// source: https://stackoverflow.com/a/4723302/1806628
function redirectToHTTPS() {
if (location.protocol !== 'https:') {
location.href = 'https:' + window.location.href.substring(window.location.protocol.length)
}
}
// converts a cents array into a uint8 array for the mnlgtun exporter
function centsTableToMnlgBinary(centsTableIn) {
const dataSize = centsTableIn.length * 3
const data = new Uint8Array(dataSize)
let dataIndex = 0
centsTableIn.forEach((c) => {
// restrict to valid values
let cents = c
if (cents < 0) cents = 0
else if (cents >= MNLG_MAXCENTS) cents = MNLG_MAXCENTS
const semitones = cents / 100.0
const microtones = Math.trunc(semitones)
const u16a = new Uint16Array([Math.round(0x8000 * (semitones - microtones))])
const u8a = new Uint8Array(u16a.buffer)
data[dataIndex] = microtones
data[dataIndex + 1] = u8a[1]
data[dataIndex + 2] = u8a[0]
dataIndex += 3
})
return data
}
// converts a mnlgtun binary string into an array of cents
function mnlgBinaryToCents(binaryData) {
const centsOut = []
const tuningSize = binaryData.length / 3
for (let i = 0; i < tuningSize; i++) {
const str = binaryData.slice(i * 3, i * 3 + 3)
const hundreds = str.charCodeAt(0) * 100
let tens = new Uint8Array([str.charCodeAt(2), str.charCodeAt(1)])
tens = Math.round((parseInt(new Uint16Array(tens.buffer)) / 0x8000) * 100)
centsOut.push(hundreds + tens)
}
return centsOut
}
// cps_combinations()
// adapted from https://www.geeksforgeeks.org/print-all-possible-combinations-of-r-elements-in-a-given-array-of-size-n/
function cps_combinations(factors, data, start, end, index, cc, products) {
// Current combination is ready to be printed, print it
if (index == cc) {
var combination = []
for (let j = 0; j < cc; j++) {
combination.push(data[j])
}
products.push(
combination.reduce(function (accumulator, currentValue) {
return accumulator * currentValue
})
)
}
// replace index with all possible elements. The condition "end-i+1 >= cc-index" makes sure that including one element at index will make a combination with remaining elements at remaining positions
for (let i = start; i <= end && end - i + 1 >= cc - index; i++) {
data[index] = factors[i]
cps_combinations(factors, data, i + 1, end, index + 1, cc, products)
}
}
// Combination Product Set function
// returns all combination products of size cc from factors array
function cps(factors, cc) {
let products = []
// store all combinations one by one
let data = new Array(cc)
// Print all combination using temporary array 'data[]'
cps_combinations(factors, data, 0, factors.length - 1, 0, cc, products)
return products
}
// scaleSort()
// takes an array of lines and returns it in ascending order
function scaleSort(scale = []) {
return scale.sort(function (a, b) {
return line_to_decimal(a) - line_to_decimal(b)
})
}
const isSimpleKeypress = (event) => {
return !(event.ctrlKey || event.shiftKey || event.altKey || event.metaKey || event.repeat)
}
================================================
FILE: src/js/keymap.js
================================================
/**
* keymap.js
* International keyboard layouts
*/
// prettier-ignore
var Layouts = {
// English QWERTY Layout
//
// <\> is placed to the right of <'> because on ISO (EU) variants it's there.
// The ANSI (US) variant places it to the right of <]>, but it's a less useful
// position so it can be ignored.
EN: [
"1234567890-=",
"QWERTYUIOP[]",
"ASDFGHJKL;'\\",
"ZXCVBNM,./",
],
// Hungarian QWERTZ layout
HU: [
"123456789ñ/=",
"QWERTZUIOP[]",
"ASDFGHJKL;'\\",
"YXCVBNM,.-",
],
// Dvorak keyboard
DK: [
"1234567890-=",
"',.PYFGCRL/@",
"AOEUIDHTNS-\\",
";QJKXBMWVZ",
],
// Programmer Dvorak keyboard
PK: [
"&7531902468#",
";,.PYFGCRL/@",
"AOEUIDHTNS-\\",
"'QJKXBMWVZ",
],
// Colemak keyboard
CO: [
"1234567890-=",
"QWFPGJLUY;[]",
"ARSTDHNEIO'\\",
"ZXCVBKM,./"
],
// Colemak DH-m keyboard
CO_DH: [
"1234567890-=",
"QWFPBJLUY;[]\\",
"ARSTGMNEIO'",
"ZXCDVKH,./"
]
};
// Map of irregular keycodes
//
// This website can be used to display the 'which' value for a given key:
//
// https://keycode.info
//
// prettier-ignore
var Keycodes = {
";": 186,
"=": 187,
",": 188,
"-": 189,
".": 190,
"/": 191,
"ñ": 192,
"[": 219,
"\\": 220,
"]": 221,
"'": 222,
"&": 166,
"#": 163,
}
// Build Keymap from Layouts
var Keymap = {}
for (var id in Layouts) {
Keymap[id] = buildKeymapFromLayout(Layouts[id])
}
function buildKeymapFromLayout(rows) {
var map = {}
for (var r = rows.length - 1; r >= 0; r--) {
var row = rows[r]
var rowId = rows.length - r - 2
for (var c = 0; c < row.length; c++) {
var keycode = Keycodes[row.charAt(c)] || row.charCodeAt(c)
map[keycode] = [rowId, c]
}
}
return map
}
================================================
FILE: src/js/midi/commands.js
================================================
const setPitchBendLimit = (channel, semitones) => {
return [
(commands.cc << 4) | (channel - 1),
cc.registeredParameterLSB,
0,
(commands.cc << 4) | (channel - 1),
cc.registeredParameterMSB,
0,
(commands.cc << 4) | (channel - 1),
cc.dataEntry,
semitones,
(commands.cc << 4) | (channel - 1),
cc.registeredParameterLSB,
127,
(commands.cc << 4) | (channel - 1),
cc.registeredParameterMSB,
127
]
}
const pitchBendAmountToDataBytes = (pitchBendAmount) => {
const realValue = pitchBendAmount - pitchBendMin
return [realValue & 0b01111111, (realValue >> 7) & 0b01111111]
}
const bendPitch = (channel, pitchBendAmount) => {
return [
...[(commands.pitchbend << 4) | (channel - 1)],
...pitchBendAmountToDataBytes(pitchBendAmount)
]
}
const noteOn = (channel, note, pitchBendAmount = null, velocity = 127) => {
return [
...(pitchBendAmount !== null ? bendPitch(channel, pitchBendAmount) : []),
...[(commands.noteOn << 4) | (channel - 1), note, velocity]
]
}
const noteOff = (channel, note, velocity = 127) => {
return [(commands.noteOff << 4) | (channel - 1), note, velocity]
}
================================================
FILE: src/js/midi/constants.js
================================================
const whiteOnlyMap = {
0: 25,
2: 26,
4: 27,
5: 28,
7: 29,
9: 30,
11: 31,
12: 32,
14: 33,
16: 34,
17: 35,
19: 36,
21: 37,
23: 38,
24: 39,
26: 40,
28: 41,
29: 42,
31: 43,
33: 44,
35: 45,
36: 46,
38: 47,
40: 48,
41: 49,
43: 50,
45: 51,
47: 52,
48: 53,
50: 54,
52: 55,
53: 56,
55: 57,
57: 58,
59: 59,
60: 60,
62: 61,
64: 62,
65: 63,
67: 64,
69: 65,
71: 66,
72: 67,
74: 68,
76: 69,
77: 70,
79: 71,
81: 72,
83: 73,
84: 74,
86: 75,
88: 76,
89: 77,
91: 78,
93: 79,
95: 80,
96: 81,
98: 82,
100: 83,
101: 84,
103: 85,
105: 86,
107: 87,
108: 88,
110: 89,
112: 90,
113: 91,
115: 92,
117: 93,
119: 94,
120: 95,
122: 96,
124: 97,
125: 98,
127: 99
}
// https://www.midi.org/specifications/item/table-1-summary-of-midi-message
const commands = {
noteOn: 0b1001,
noteOff: 0b1000,
aftertouch: 0b1010,
pitchbend: 0b1110,
cc: 0b1011
}
// https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2
// http://www.nortonmusic.com/midi_cc.html
const cc = {
dataEntry: 6,
sustain: 64,
registeredParameterLSB: 100,
registeredParameterMSB: 101
}
/// 440Hz A4
const referenceNote = {
frequency: 440,
id: 69
}
const pitchBendMin = 1 - (1 << 14) / 2 // -8191
const pitchBendMax = (1 << 14) / 2 // 8192
// settings for MIDI OUT ports
const defaultInputData = {
enabled: true,
channels: [
{ id: 1, enabled: true, pitchBendAmount: 0 },
{ id: 2, enabled: true, pitchBendAmount: 0 },
{ id: 3, enabled: true, pitchBendAmount: 0 },
{ id: 4, enabled: true, pitchBendAmount: 0 },
{ id: 5, enabled: true, pitchBendAmount: 0 },
{ id: 6, enabled: true, pitchBendAmount: 0 },
{ id: 7, enabled: true, pitchBendAmount: 0 },
{ id: 8, enabled: true, pitchBendAmount: 0 },
{ id: 9, enabled: true, pitchBendAmount: 0 },
{ id: 10, enabled: false, pitchBendAmount: 0 }, // drum channel
{ id: 11, enabled: true, pitchBendAmount: 0 },
{ id: 12, enabled: true, pitchBendAmount: 0 },
{ id: 13, enabled: true, pitchBendAmount: 0 },
{ id: 14, enabled: true, pitchBendAmount: 0 },
{ id: 15, enabled: true, pitchBendAmount: 0 },
{ id: 16, enabled: true, pitchBendAmount: 0 }
]
}
// settings for MIDI IN ports
const defaultOutputData = {
enabled: false,
channels: [
{ id: 1, enabled: true, pitchBendAmount: 0 },
{ id: 2, enabled: false, pitchBendAmount: 0 },
{ id: 3, enabled: false, pitchBendAmount: 0 },
{ id: 4, enabled: false, pitchBendAmount: 0 },
{ id: 5, enabled: false, pitchBendAmount: 0 },
{ id: 6, enabled: false, pitchBendAmount: 0 },
{ id: 7, enabled: false, pitchBendAmount: 0 },
{ id: 8, enabled: false, pitchBendAmount: 0 },
{ id: 9, enabled: false, pitchBendAmount: 0 },
{ id: 10, enabled: false, pitchBendAmount: 0 },
{ id: 11, enabled: false, pitchBendAmount: 0 },
{ id: 12, enabled: false, pitchBendAmount: 0 },
{ id: 13, enabled: false, pitchBendAmount: 0 },
{ id: 14, enabled: false, pitchBendAmount: 0 },
{ id: 15, enabled: false, pitchBendAmount: 0 },
{ id: 16, enabled: false, pitchBendAmount: 0 }
]
}
const octaveRatio = 2
const semitonesPerOctave = 12
const maxBendingDistanceInSemitones = 12
const centsPerOctave = 1200
const middleC = 60
const drumChannel = 10 // when counting from 1
const allMidiKeys = [...Array(128).keys()] // [0, 1, 2, ..., 127]
const whiteMidiKeys = Object.keys(whiteOnlyMap).map((id) => parseInt(id))
const blackMidiKeys = R.difference(allMidiKeys, whiteMidiKeys)
================================================
FILE: src/js/midi/math.js
================================================
const moveNUnits = (ratioOfSymmetry, divisionsPerRatio, n, frequency) => {
// return frequency * ratioOfSymmetry ** (n / divisionsPerRatio)
return Decimal.mul(frequency, Decimal.pow(ratioOfSymmetry, Decimal.div(n, divisionsPerRatio)))
}
const getDistanceInUnits = (ratioOfSymmetry, divisionsPerRatio, freq2, freq1) => {
// return divisionsPerRatio * Math.log(freq2 / freq1, ratioOfSymmetry)
return Decimal.mul(divisionsPerRatio, Decimal.log(Decimal.div(freq2, freq1), ratioOfSymmetry))
}
const moveNSemitones = (n, frequency) => {
return moveNUnits(octaveRatio, semitonesPerOctave, n, frequency)
}
const getDistanceInSemitones = (freq2, freq1) => {
return getDistanceInUnits(octaveRatio, semitonesPerOctave, freq2, freq1)
}
const bendingRatio = moveNSemitones(maxBendingDistanceInSemitones, 1)
const getBendingDistance = (freq2, freq1) => {
return getDistanceInUnits(bendingRatio, pitchBendMax, freq2, freq1)
}
const getNoteFrequency = (midinote) => {
return moveNSemitones(
Decimal.sub(R.clamp(0, 127, midinote), referenceNote.id),
referenceNote.frequency
)
}
const getNoteId = (frequency) => {
return Decimal.floor(
Decimal.add(getDistanceInSemitones(frequency, referenceNote.frequency), referenceNote.id)
)
}
================================================
FILE: src/js/midi/midi.js
================================================
/**
* midi.js
* Capture MIDI input for synth
*/
const deviceChannelInfo = {}
const getNameFromPort = (port) => {
const { name, version, manufacturer } = port
return `${name} (version ${version}) ${manufacturer}`
}
class MIDI extends EventEmitter {
constructor() {
super()
this._ = {
inited: false,
supported: false,
devices: {
inputs: {},
outputs: {}
},
whiteOnly: false
}
}
set whiteOnly(value) {
this._.whiteOnly = value
allMidiKeys.forEach((note) => {
for (let channel = 1; channel <= 16; channel++) {
this.emit('note off', note, 1, channel)
}
})
}
async init() {
if (!this._.inited) {
this._.inited = true
const enableMidiSupport = (midiAccess) => {
this._.supported = true
midiAccess.onstatechange = (event) => {
initPort(event.port)
}
const inputs = midiAccess.inputs.values()
for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
initPort(input.value)
}
const outputs = midiAccess.outputs.values()
for (let output = outputs.next(); output && !output.done; output = outputs.next()) {
initPort(output.value)
}
}
const initPort = (port) => {
const { devices } = this._
if (port.type === 'input') {
if (!devices.inputs[port.id]) {
devices.inputs[port.id] = {
port,
name: getNameFromPort(port),
...R.clone(defaultInputData)
}
}
devices.inputs[port.id].connected = false
if (port.state === 'connected') {
if (port.connection === 'closed') {
port.open()
} else if (port.connection === 'open') {
port.onmidimessage = onMidiMessage(devices.inputs[port.id])
devices.inputs[port.id].connected = true
}
}
} else if (port.type === 'output') {
if (!devices.outputs[port.id]) {
devices.outputs[port.id] = {
port,
name: getNameFromPort(port),
...R.clone(defaultOutputData)
}
}
if (port.state === 'connected') {
if (port.connection === 'closed') {
port.open()
} else if (port.connection === 'open') {
devices.outputs[port.id].connected = true
}
}
}
this.emit('update')
}
const onMidiMessage = (device) => (event) => {
if (device.enabled) {
const { whiteOnly } = this._
const [data, ...params] = event.data
const cmd = data >> 4
const channel = data & 0x0f
if (device.channels[channel]?.enabled === true) {
switch (cmd) {
case commands.noteOff:
{
const [note, velocity] = params
if (whiteOnly) {
if (whiteMidiKeys.includes(note)) {
this.emit('note off', whiteOnlyMap[note], velocity, channel)
}
} else {
this.emit('note off', note, velocity, channel)
}
}
break
case commands.noteOn:
{
const [note, velocity] = params
if (whiteOnly) {
if (whiteMidiKeys.includes(note)) {
this.emit(
'note on',
whiteOnlyMap[note],
state.get('midi velocity sensing') ? velocity : 127,
channel
)
}
} else {
this.emit(
'note on',
note,
state.get('midi velocity sensing') ? velocity : 127,
channel
)
}
}
break
case commands.aftertouch:
{
const [note, pressure] = params
if (whiteOnly) {
if (whiteMidiKeys.includes(note)) {
this.emit('aftertouch', whiteOnlyMap[note], (pressure / 128) * 100, channel)
}
} else {
this.emit('aftertouch', note, (pressure / 128) * 100, channel)
}
}
break
case commands.pitchbend:
{
const [low, high] = params
this.emit('pitchbend', (((high << 7) | low) / 0x3fff - 1) * 100)
}
break
case commands.cc:
{
const [cmd, value] = params
switch (cmd) {
case cc.sustain:
this.emit('sustain', value >= 64)
break
}
}
break
}
}
}
}
if (navigator.requestMIDIAccess) {
const midiAccess = await navigator.requestMIDIAccess({ sysex: false })
enableMidiSupport(midiAccess)
this.emit('ready')
} else {
this.emit('blocked')
}
}
}
toggleDevice(type, deviceId, newValue = null) {
const { devices } = this._
const device = devices[`${type}s`][deviceId]
device.enabled = newValue === null ? !device.enabled : newValue
if (type === 'output') {
if (device.enabled) {
device.channels.forEach((channel) => {
device.port.send(setPitchBendLimit(channel, maxBendingDistanceInSemitones))
})
} else {
device.channels.forEach((channel) => {
device.port.send(bendPitch(channel, 0))
})
}
}
this.emit('update')
}
setDevice(type, deviceId, newValue) {
this.toggleDevice(type, deviceId, newValue)
}
toggleChannel(type, deviceId, channelId, newValue = null) {
const { devices } = this._
const device = devices[`${type}s`][deviceId]
const channel = device.channels.find(({ id }) => id === channelId)
newValue = newValue === null ? !channel.enabled : newValue
if (channel.enabled !== newValue) {
channel.enabled = newValue
this.emit('update')
}
}
setChannel(type, deviceId, channelId, newValue) {
this.toggleChannel(type, deviceId, channelId, newValue)
}
getEnabledOutputs() {
return Object.values(this._.devices.outputs).filter(({ enabled, channels }) => {
return enabled === true && channels.find(({ enabled }) => enabled === true) !== undefined
})
}
getLowestEnabledChannel(channels) {
return channels.find(({ enabled }) => enabled === true)
}
playFrequency(frequency = 0) {
const devices = this.getEnabledOutputs()
if (devices.length) {
devices.forEach(({ port, channels }) => {
const channel = channels.find(({ enabled }) => enabled === true)
if (!deviceChannelInfo[port.id]) {
deviceChannelInfo[port.id] = {}
}
if (!deviceChannelInfo[port.id][channel]) {
deviceChannelInfo[port.id][channel] = {
pressedNoteIds: []
}
}
if (frequency === 0) {
if (deviceChannelInfo[port.id][channel].pressedNoteIds.length) {
port.send(
deviceChannelInfo[port.id][channel].pressedNoteIds.flatMap((noteId) => {
return noteOff(channel, noteId)
})
)
deviceChannelInfo[port.id][channel].pressedNoteIds = []
}
} else {
const noteId = parseInt(getNoteId(frequency).toString())
const pitchbendAmount = parseFloat(
getBendingDistance(frequency, getNoteFrequency(noteId)).toString()
)
port.send(noteOn(channel, noteId, pitchbendAmount))
deviceChannelInfo[port.id][channel].pressedNoteIds.push(noteId)
}
})
}
}
stopFrequency() {
this.playFrequency(0)
}
isSupported() {
return this._.supported
}
}
// -------------------------------------------
const midi = new MIDI()
jQuery(() => {
const midiEnablerBtn = jQuery('#midi-enabler')
midi
.on('blocked', () => {
midiEnablerBtn
.prop('disabled', false)
.removeClass('btn-success')
.addClass('btn-danger')
.text('off (blocked)')
})
.on('note on', (note, velocity, channel) => {
synth.noteOn(note, velocity)
})
.on('note off', (note, velocity, channel) => {
synth.noteOff(note)
})
.on('update', () => {
if (state.get('midi modal visible')) {
state.set('midi modal visible', true, true)
}
})
midiEnablerBtn.on('click', async () => {
await midi.init()
if (midi.isSupported()) {
state.set('midi enabled', true)
}
})
})
================================================
FILE: src/js/midi/ui.js
================================================
const MidiChannel = ({ type, deviceId, channelId, enabled }) => {
const template = document.createElement('template')
template.innerHTML = `
`
const content = template.content
channels.forEach(({ id, enabled }) => {
content
.querySelector('.channels')
.appendChild(MidiChannel({ type, deviceId, channelId: id, enabled }))
})
content.getElementById(`${type}--${name}`).addEventListener('change', (e) => {
const isEnabled = e.target.checked
midi.setDevice(type, deviceId, isEnabled)
})
return content
}
const renderMidiInputsTo = (container) => {
const { inputs } = midi._.devices
container.innerHTML = ''
Object.values(inputs).forEach((input) => {
container.appendChild(MidiDevice({ type: 'input', deviceId: input.port.id, ...input }))
})
}
const renderMidiOutputsTo = (container) => {
const { outputs } = midi._.devices
container.innerHTML = ''
Object.values(outputs).forEach((output) => {
container.appendChild(MidiDevice({ type: 'output', deviceId: output.port.id, ...output }))
})
}
const renderMidiSettingsTo = (container) => {
const { whiteOnly } = midi._
const whiteModeSwitch = container.querySelector('#input_midi_whitemode')
whiteModeSwitch.checked = whiteOnly
whiteModeSwitch.addEventListener('change', (e) => {
midi.whiteOnly = e.target.checked
})
}
================================================
FILE: src/js/modifiers.js
================================================
/**
* TUNING DATA MODIFIERS
*/
// stretch/compress tuning
function modify_stretch() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
var stretch_ratio = parseFloat(jQuery('#input_stretch_ratio').val()) // amount of stretching, ratio
// split user data into individual lines
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
// strip out the unusable lines, assemble a multi-line string which will later replace the existing tuning data
let new_tuning_lines = []
for (let i = 0; i < lines.length; i++) {
switch (getLineType(lines[i])) {
case 'invalid':
alert('Scale data looks invalid: ' + lines[i])
return false
case 'cents':
new_tuning_lines.push((parseFloat(lines[i]) * stretch_ratio).toFixed(5))
break
case 'n of edo':
new_tuning_lines.push((n_of_edo_to_cents(lines[i]) * stretch_ratio).toFixed(5))
break
case 'ratio':
new_tuning_lines.push((ratio_to_cents(lines[i]) * stretch_ratio).toFixed(5))
}
}
// update tuning input field with new tuning
jQuery('#txt_tuning_data').val(new_tuning_lines.join(unix_newline))
parse_tuning_data()
jQuery('#modal_modify_stretch').dialog('close')
// success
return true
}
// random variance
function modify_random_variance() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
var cents_max_variance = parseFloat(jQuery('#input_cents_max_variance').val()) // maximum amount of variance in cents
var vary_period = document.getElementById('input_checkbox_vary_period').checked
// split user data into individual lines
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
// strip out the unusable lines, assemble a multi-line string which will later replace the existing tuning data
let new_tuning_lines = []
for (let i = 0; i < lines.length; i++) {
// only apply random variance if the line is not the period, or vary_period is true
if (vary_period || i < lines.length - 1) {
// get a cents offset to add later. ranges from -cents_max_variance to cents_max_variance
var random_variance = Math.random() * cents_max_variance * 2 - cents_max_variance
// line contains a period, so it should be a value in cents
if (lines[i].toString().includes('.')) {
new_tuning_lines.push((parseFloat(lines[i]) + random_variance).toFixed(5))
}
// line doesn't contain a period, so it is a ratio
else {
new_tuning_lines.push((ratio_to_cents(lines[i]) + random_variance).toFixed(5))
}
}
// last line is a period and we're not applying random variance to it
else {
new_tuning_lines.push(lines[i])
}
}
// update tuning input field with new tuning
jQuery('#txt_tuning_data').val(new_tuning_lines.join(unix_newline))
parse_tuning_data()
jQuery('#modal_modify_random_variance').dialog('close')
// success
return true
}
// mode
function modify_mode() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
var mode = jQuery('#input_modify_mode').val().split(' ')
// check user input for invalid items
for (let i = 0; i < mode.length; i++) {
mode[i] = parseInt(mode[i])
if (isNaN(mode[i]) || mode[i] < 1) {
alert(
'Your mode should contain a list of positive integers, seperated by spaces. E.g.' +
unix_newline +
'5 5 1 3 1 2'
)
return false
}
}
// split user's scale data into individual lines
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
console.log(lines)
console.log(mode)
// mode_type will be either intervals (e.g. 2 2 1 2 2 2 1) or from_base (e.g. 2 4 5 7 9 11 12)
var mode_type = jQuery("#modal_modify_mode input[type='radio']:checked").val()
if (mode_type == 'intervals' || mode_type == 'mos') {
// get the total number of notes in the mode
var mode_sum = mode.reduce(function (a, b) {
return a + b
}, 0)
// number of notes in the mode should equal the number of lines in the scale data field
if (mode_sum != lines.length) {
alert(
"Your mode doesn't add up to the same size as the current scale." +
unix_newline +
"E.g. if you have a 5 note scale, mode 2 2 1 is valid because 2+2+1=5. But mode 2 2 2 is invalid because 2+2+2 doesn't equal 5."
)
return false
}
// strip out the unusable lines, assemble a multi-line string which will later replace the existing tuning data
var new_tuning = ''
var note_count = 1
var mode_index = 0
for (let i = 0; i < lines.length; i++) {
if (mode[mode_index] == note_count) {
new_tuning = new_tuning + lines[i]
// add a newline for all lines except the last
if (i < lines.length - 1) {
new_tuning += newline
}
mode_index++
note_count = 0
}
note_count++
}
}
// if ( mode_type == "from_base" ) {
else {
// number of notes in the mode should equal the number of lines in the scale data field
if (mode[mode.length - 1] != lines.length) {
alert(
"Your mode isn't the same size as the current scale." +
unix_newline +
'E.g. if you have a 5 note scale, mode 2 4 5 is valid because the final degree is 5. But mode 2 4 6 is invalid because 6 is greater than 5.'
)
return false
}
// strip out the unusable lines, assemble a multi-line string which will later replace the existing tuning data
var new_tuning = ''
for (let i = 0; i < mode.length; i++) {
new_tuning += lines[mode[i] - 1]
// add a newline for all lines except the last
if (i < mode.length - 1) {
new_tuning += unix_newline
}
}
}
// update tuning input field with new tuning
jQuery('#txt_tuning_data').val(new_tuning)
parse_tuning_data()
jQuery('#modal_modify_mode').dialog('close')
// success
return true
}
// sync beating
function modify_sync_beating() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
if (R.isEmpty(jQuery('#input_modify_sync_beating_bpm').val())) {
alert('Please enter a BPM value.')
return false
}
// get the fundamental frequency of the scale
var fundamental = jQuery('#input_modify_sync_beating_bpm').val() / 60
console.log(fundamental)
var resolution = jQuery('#select_sync_beating_resolution').val()
console.log(resolution)
// loop through all in the scale, convert to ratio, then quantize to fundamental, then convert to cents
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
console.log(lines)
var new_tuning = ''
for (let i = 0; i < lines.length; i++) {
lines[i] = line_to_decimal(lines[i])
new_tuning += R.toString(Math.round(lines[i] * resolution)) + '/' + resolution + unix_newline
}
new_tuning = new_tuning.trim() // remove final newline
console.log(new_tuning)
// set tuning base frequency to some multiple of the fundamental, +/- 1 tritone from the old base frequency
var basefreq_lowbound = jQuery('#txt_base_frequency').val() * 0.7071067
var basefreq = fundamental
do {
basefreq = basefreq * 2
} while (basefreq < basefreq_lowbound)
// update fields and parse
jQuery('#txt_tuning_data').val(new_tuning)
jQuery('#txt_base_frequency').val(basefreq)
parse_tuning_data()
jQuery('#modal_modify_sync_beating').dialog('close')
// success
return true
}
// rotate scale
function modify_rotate() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
// catch missing scale data
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
// split scale data into individual lines
var lines = jQuery('#txt_tuning_data').val().trim().split(unix_newline)
// get transposing degree, transposing interval and equave
let degree = parseInt(jQuery('#input_rotate_new_1_1').val())
let transposer = negateLine(lines[degree])
let equave = lines[lines.length - 1]
let rotatedLines = []
// transpose lines, mod equave
// start on degree after transposer/new root, cycle around and stop before transposer
for (let i = 1; i < lines.length; i++) {
let deg = (i + degree) % lines.length
let scaleIndex = i - 1
rotatedLines[scaleIndex] = moduloLine(transposeLine(lines[deg], transposer), equave)
}
// add equave
rotatedLines.push(equave)
// update tuning input field with new tuning
jQuery('#txt_tuning_data').val(rotatedLines.join(unix_newline))
parse_tuning_data()
jQuery('#modal_modify_rotate').dialog('close')
// success
return true
}
// approximate rationals
function modify_replace_with_approximation() {
var degree_selected = parseInt(jQuery('#input_scale_degree').val())
if (degree_selected < tuning_table.note_count) {
var tuning_data = document.getElementById('txt_tuning_data')
var lines = tuning_data.value.split(newlineTest)
var aprxs = document.getElementById('approximation_selection')
var approximation = aprxs.options[aprxs.selectedIndex].text
approximation = approximation.slice(0, approximation.indexOf('|')).trim()
if (degree_selected - 1 < lines.length && line_to_decimal(approximation)) {
lines[degree_selected - 1] = approximation
} else {
lines.push(approximation)
}
var lines_to_text = ''
lines.forEach(function (item, index, array) {
lines_to_text += lines[index]
if (index + 1 < array.length) lines_to_text += newline
})
tuning_data.value = lines_to_text
parse_tuning_data()
if (degree_selected < tuning_table.note_count - 1) {
jQuery('#input_scale_degree').val(degree_selected + 1)
jQuery('#input_scale_degree').trigger('change')
}
// success
return true
}
// invalid scale degree
return false
}
// update list of rationals to choose from
function modify_update_approximations() {
jQuery('#approximation_selection').empty()
if (!R.isEmpty(current_approximations)) {
var interval = line_to_decimal(jQuery('#input_interval_to_approximate').val())
var mincentsd = parseFloat(jQuery('#input_min_error').val())
var maxcentsd = parseFloat(jQuery('#input_max_error').val())
var minprime = parseInt(jQuery(' #input_approx_min_prime').val())
var maxprime = parseInt(jQuery(' #input_approx_max_prime').val())
var semiconvergents = !document.getElementById('input_show_convergents').checked
if (minprime < 2) {
minprime = 2
jQuery('#input_approx_min_prime').val(2)
}
if (maxprime > 7919) {
maxprime = 7919
jQuery('#input_approx_max_prime').val(7919)
}
if (mincentsd < 0) mincentsd = 0
if (maxcentsd < 0) maxcentsd = 0
var menulength = semiconvergents
? current_approximations.ratios.length
: current_approximations.convergent_indicies.length
var index
for (var i = 0; i < menulength; i++) {
index = semiconvergents ? i : current_approximations.convergent_indicies[i]
var n = parseInt(current_approximations.numerators[index])
var d = parseInt(current_approximations.denominators[index])
var prime_limit = current_approximations.ratio_limits[index]
var fraction_str = current_approximations.ratios[index]
var fraction = n / d
var cents_deviation = decimal_to_cents(fraction) - decimal_to_cents(interval)
var centsdabs = Math.abs(cents_deviation)
var cents_rounded = Math.round(10e6 * cents_deviation) / 10e6
var centsdsgn
if (cents_deviation / centsdabs >= 0) centsdsgn = '+'
else centsdsgn = ''
var description =
fraction_str +
' | ' +
centsdsgn +
cents_rounded.toString() +
'c | ' +
prime_limit +
'-limit'
if (!interval) {
jQuery('#approximation_selection').append(
''
)
break
} else if (interval == fraction && interval) {
// for cases like 1200.0 == 2/1
jQuery('#approximation_selection').append('')
break
} else if (
centsdabs >= mincentsd &&
centsdabs <= maxcentsd &&
prime_limit >= minprime &&
prime_limit <= maxprime
) {
jQuery('#approximation_selection').append('')
}
}
if (document.getElementById('approximation_selection').options.length === 0) {
semiconvergents
? jQuery('#approximation_selection').append(
''
)
: jQuery('#approximation_selection').append(
''
)
}
}
}
// approximate by harmonics
function modify_approximate_harmonics() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
if (R.isEmpty(jQuery('#input_approx_harm_denominator').val())) {
alert('Please enter a denominator value.')
return false
}
var denominator = jQuery('#input_approx_harm_denominator').val()
// loop through all intervals and approximate each with nearest harmonic
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
console.log(lines)
var new_tuning = ''
for (let i = 0; i < lines.length; i++) {
lines[i] = line_to_decimal(lines[i])
new_tuning += R.toString(Math.round(lines[i] * denominator)) + '/' + denominator + unix_newline
}
new_tuning = new_tuning.trim() // remove final newline
console.log(new_tuning)
// update fields and parse
jQuery('#txt_tuning_data').val(new_tuning)
parse_tuning_data()
jQuery('#modal_approximate_harmonics').dialog('close')
// success
return true
}
function modify_approximate_subharmonics() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
if (R.isEmpty(jQuery('#input_approx_subharm_numerator').val())) {
alert('Please enter a numerator value.')
return false
}
var numerator = jQuery('#input_approx_subharm_numerator').val()
// loop through all intervals and approximate each with nearest harmonic
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
console.log(lines)
var new_tuning = ''
for (let i = 0; i < lines.length; i++) {
lines[i] = line_to_decimal(lines[i])
new_tuning += numerator + '/' + R.toString(Math.round(numerator / lines[i])) + unix_newline
}
new_tuning = new_tuning.trim() // remove final newline
console.log(new_tuning)
// update fields and parse
jQuery('#txt_tuning_data').val(new_tuning)
parse_tuning_data()
jQuery('#modal_approximate_subharmonics').dialog('close')
// success
return true
}
function modify_equalize() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
if (R.isEmpty(jQuery('#txt_tuning_data').val())) {
alert('No scale data to modify.')
return false
}
if (R.isEmpty(jQuery('#input_equalize_divisions').val())) {
alert('Please enter the number of divisions.')
return false
}
// loop through all intervals and approximate each with nearest harmonic
var lines = document.getElementById('txt_tuning_data').value.split(newlineTest)
console.log(lines)
var new_tuning = ''
var divisions = jQuery('#input_equalize_divisions').val()
var step_size = line_to_cents(lines[lines.length - 1]) / divisions
for (let i = 0; i < lines.length - 1; i++) {
// round to nearest step_size
lines[i] = Math.round(line_to_cents(lines[i]) / step_size) * step_size
// ensure that line result has a . at the end
if (R.toString(lines[i]).indexOf('.') == -1) {
lines[i] = R.toString(lines[i]) + '.'
}
new_tuning += lines[i] + unix_newline
}
new_tuning += lines[lines.length - 1] // leave last line unchanged
console.log(new_tuning)
// update fields and parse
jQuery('#txt_tuning_data').val(new_tuning)
parse_tuning_data()
jQuery('#modal_equalize').dialog('close')
// success
return true
}
function modify_octave_reduce() {
// remove white space from tuning data field
trimSelf('#txt_tuning_data')
// get current scale as array
var scale = jQuery('#txt_tuning_data').val()
var octave = jQuery('#input_reduce_octave').val()
// bail if input invalid
if (R.isEmpty(scale)) {
alert('No scale data to modify.')
return false
}
if (getLineType(octave) === LINE_TYPE.INVALID) {
alert(
'Warning: Interval to reduce to is invalid. Input must be ratio (e.g. 2/1), cents (e.g. 1200.0), n\\m (e.g. 12\\12) or decimal (e.g. 2,0)'
)
return false
}
scale = scale.split(unix_newline)
// each line modulo with the octave
for (let i = 0; i < scale.length; i++) {
if (scale[i] !== octave) {
scale[i] = moduloLine(scale[i], octave)
}
}
// sort resulting scale in ascending order
if (document.getElementById('input_reduce_also_sort').checked) {
scale = scaleSort(scale)
}
// update fields and parse
jQuery('#txt_tuning_data').val(scale.join(unix_newline))
parse_tuning_data()
jQuery('#modal_modify_octave_reduce').dialog('close')
// success
return true
}
================================================
FILE: src/js/scaleworkshop.js
================================================
/**
* INIT
*/
// check if coming from a Back/Forward history navigation.
// need to reload the page so that url params take effect
jQuery(window).on('popstate', function () {
console.log('Back/Forward navigation detected - reloading page')
location.reload(true)
})
if (
window.location.hostname.endsWith('.github.com') ||
window.location.hostname.endsWith('sevish.com')
) {
redirectToHTTPS()
}
/**
* GLOBALS
*/
const APP_TITLE = 'Scale Workshop 1.5'
const TUNING_MAX_SIZE = 128
let newline = localStorage && localStorage.getItem('newline') === 'windows' ? '\r\n' : '\n'
const newlineTest = /\r?\n/
const unix_newline = '\n'
var tuning_table = {
scale_data: [], // an array containing list of intervals input by the user
tuning_data: [], // an array containing the same list above converted to decimal format
note_count: 0, // number of values stored in tuning_data
freq: [], // an array containing the frequency for each MIDI note
cents: [], // an array containing the cents value for each MIDI note
decimal: [], // an array containing the frequency ratio expressed as decimal for each MIDI note
base_frequency: 440, // init val
base_midi_note: 69, // init val
description: '',
filename: ''
}
var key_colors = [
'white',
'black',
'white',
'white',
'black',
'white',
'black',
'white',
'white',
'black',
'white',
'black'
]
var current_approximations = {
convergent_indicies: [], // indicies of the convergent ratios
numerators: [], // numerators of approximations
denominators: [], // denominators of approximations
ratios: [], // the ratios combined
numerator_limits: [], // the prime limit of each numerator
denominator_limits: [], // the prime limit of each denominator
ratio_limits: [] // the prime limit of each ratio
}
var prime_counter = [0, 10]
/**
* SCALE WORKSHOP FUNCTIONS
*/
// take a tuning, do loads of calculations, then output the data to tuning_table
function generate_tuning_table(tuning) {
var base_frequency = tuning_table['base_frequency']
var base_midi_note = tuning_table['base_midi_note']
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
var offset = i - base_midi_note
var quotient = Math.floor(offset / (tuning.length - 1))
var remainder = offset % (tuning.length - 1)
if (remainder < 0) remainder += tuning.length - 1
var period = tuning[tuning.length - 1]
// "decimal" here means a frequency ratio, but stored in decimal format
var decimal = tuning[remainder] * Math.pow(period, quotient)
// store the data in the tuning_table object
tuning_table['freq'][i] = base_frequency * decimal
tuning_table['cents'][i] = decimal_to_cents(decimal)
tuning_table['decimal'][i] = decimal
}
}
function set_key_colors(list) {
// check if the list of colors is empty
if (R.isEmpty(list)) {
// bail, leaving the previous colors in place
return false
}
key_colors = list.split(' ')
// get all the tuning table key cell elements
var ttkeys = jQuery('#tuning-table td.key-color')
// for each td.key-color
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
// get the number representing this key color, with the first item being 0
var keynum = (i - tuning_table['base_midi_note']).mod(key_colors.length)
// set the color of the key
jQuery(ttkeys[i]).attr('style', 'background-color: ' + key_colors[keynum] + ' !important')
}
}
/**
* parse_url()
*/
function parse_url() {
// ?name=16%20equal%20divisions%20of%202%2F1&data=75.%0A150.%0A225.%0A300.%0A375.%0A450.%0A525.%0A600.%0A675.%0A750.%0A825.%0A900.%0A975.%0A1050.%0A1125.%0A1200.&freq=440&midi=69&vert=5&horiz=1&colors=white%20black%20white%20black%20white%20black%20white%20white%20black%20white%20black%20white%20black%20white%20black%20white&waveform=sine&env=pad
var url = new URL(window.location.href)
// get data from url params, and use sane defaults for tuning name, base frequency and base midi note number if data missing
var name = getSearchParamOr('', 'name', url)
var data = getSearchParamOr(false, 'data', url)
var freq = getSearchParamAsNumberOr(440, 'freq', url)
var midi = getSearchParamAsNumberOr(69, 'midi', url)
var source = getSearchParamOr('', 'source', url)
// get isomorphic keyboard mapping
var vertical = getSearchParamAsNumberOr(false, 'vert', url)
var horizontal = getSearchParamAsNumberOr(false, 'horiz', url)
// get key colours
var colors = getSearchParamOr(false, 'colors', url)
// get synth options
var waveform = getSearchParamOr(false, 'waveform', url)
var ampenv = getSearchParamOr(false, 'ampenv', url)
// bail if there is no data
if (!data) {
return false
}
// decodes HTML entities
function decodeHTML(input) {
var doc = new DOMParser().parseFromString(input, 'text/html')
return doc.documentElement.textContent
}
// parses Scala entries from the Xenharmonic Wiki
function parseWiki(str) {
var s = decodeHTML(str)
s = s.replace(/[_ ]+/g, '') // remove underscores and spaces
var a = s.split(newlineTest) // split by line into an array
a = a.filter((line) => !line.startsWith('<') && !line.startsWith('{') && !R.isEmpty(line)) // remove tag, wiki templates and blank lines
a = a.map((line) => line.split('!')[0]) // remove .scl comments
a = a.slice(2) // remove .scl metadata
return a.join(unix_newline)
}
// specially parse inputs from the Xenharmonic Wiki
if (source === 'wiki') {
data = parseWiki(data)
}
// enter the data from url in to the on-page form
jQuery('#txt_name').val(name)
jQuery('#txt_tuning_data').val(data)
jQuery('#txt_base_frequency').val(freq)
jQuery('#txt_base_midi_note').val(midi)
jQuery('#input_number_isomorphicmapping_vert').val(vertical)
jQuery('#input_number_isomorphicmapping_horiz').val(horizontal)
// if there is isomorphic keyboard mapping data, apply it
if (vertical !== false) synth.isomorphicMapping.vertical = vertical
if (horizontal !== false) synth.isomorphicMapping.horizontal = horizontal
// parse the tuning data
if (parse_tuning_data()) {
// if there are key colorings, apply them
if (colors !== false) {
jQuery('#input_key_colors').val(colors)
set_key_colors(colors)
}
// if there are synth options, apply them
if (waveform !== false) {
jQuery('#input_select_synth_waveform').val(waveform)
synth.waveform = waveform
}
if (ampenv !== false) jQuery('#input_select_synth_amp_env').val(ampenv)
// success
return true
} else {
// something probably wrong with the input data
return false
}
}
/**
* parse_tuning_data()
*/
function parse_tuning_data() {
// http://www.huygens-fokker.org/scala/scl_format.html
tuning_table['base_midi_note'] = parseInt(jQuery('#txt_base_midi_note').val())
tuning_table['base_frequency'] = parseFloat(jQuery('#txt_base_frequency').val())
tuning_table['description'] = jQuery('#txt_name').val()
tuning_table['filename'] = sanitize_filename(tuning_table['description'])
var user_tuning_data = document.getElementById('txt_tuning_data')
// check if user pasted a scala file
// we check if the first character is !
if (user_tuning_data.value.startsWith('!')) {
alert(
'Hello, trying to paste a Scala file into this app?' +
unix_newline +
"Please use the 'Import .scl' function instead or remove the first few lines (description) from the text box"
)
jQuery('#txt_tuning_data').parent().addClass('has-error')
return false
}
// split user data into individual lines
var lines = user_tuning_data.value.split(newlineTest)
// strip out the unusable lines, assemble an array of usable tuning data
tuning_table['scale_data'] = [] // initialise scale_data array
tuning_table['tuning_data'] = ['1'] // when initialised the array contains only '1' (unison)
tuning_table['note_count'] = 1
var empty = true
for (let i = 0; i < lines.length; i++) {
// check that line is not empty
if (!R.isEmpty(lines[i])) {
if (getLineType(lines[i]) === LINE_TYPE.INVALID) {
jQuery('#txt_tuning_data').parent().addClass('has-error')
return false
}
// so far so good - store the line in tuning array
tuning_table['scale_data'][tuning_table['note_count']] = lines[i] // 'scale_data' is the scale in the original format input in the text box
tuning_table['tuning_data'][tuning_table['note_count']] = line_to_decimal(lines[i]) // 'tuning_data' is the same as before but all input is converted to decimal format to make the maths easier later
tuning_table['note_count']++
// if we got to this point, then the tuning must not be empty
empty = false
}
}
if (empty) {
// if the input tuning is totally empty
console.log('no tuning data')
jQuery('#txt_tuning_data').parent().addClass('has-error')
return false
}
// finally, generate the frequency table
generate_tuning_table(tuning_table['tuning_data'])
// display generated tuning in a table on the page
jQuery('#tuning-table').empty()
jQuery('#tuning-table').append(
"
#
Freq.
Cents
Ratio
"
)
for (let i = 0; i < TUNING_MAX_SIZE; i++) {
// highlight the row which corresponds to the base MIDI note
var table_class = ''
if (i == tuning_table['base_midi_note']) {
table_class = 'info'
} else {
if ((tuning_table['base_midi_note'] - i) % (tuning_table['note_count'] - 1) == 0) {
table_class = 'warning'
}
}
// assemble the HTML for the table row
jQuery('#tuning-table').append(
"
'
)
}
jQuery('#tuning-table').append('')
set_key_colors(jQuery('#input_key_colors').val())
// scroll to reference note on the table
jQuery('#col-tuning-table').animate(
{
scrollTop:
jQuery('#tuning-table-row-' + tuning_table['base_midi_note']).position().top +
jQuery('#col-tuning-table').scrollTop()
},
600
) // 600ms scroll to reference note
jQuery('#txt_tuning_data').parent().removeClass('has-error')
// draw horizontal rule graphic
render_graphic_scale_rule()
// if has changed, convert the scale into a URL then add that URL to the browser's Back/Forward navigation
var url = get_scale_url()
if (url !== window.location.href) {
update_page_url(url)
}
// success
return true
}
/**
* TUNING IMPORT RELATED FUNCTIONS
*/
function is_file_api_supported() {
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
return true
} else {
// File API not supported
alert(
"Trying to load a file? Sorry, your browser doesn't support the HTML5 File API. Please try using a different browser."
)
return false
}
}
function import_scala_scl() {
// check File API is supported
if (is_file_api_supported()) {
// trigger load file dialog
jQuery('#scala-file').trigger('click')
}
}
function import_anamark_tun() {
// check File API is supported
if (is_file_api_supported()) {
// trigger load file dialog
jQuery('#anamark-tun-file').trigger('click')
}
}
function importMnlgtun() {
// check File API is supported
if (is_file_api_supported()) {
// trigger load file dialog
jQuery('#mnlgtun-file').trigger('click')
}
}
// after a scala file is loaded, this function will be called
function parse_imported_scala_scl(event) {
var input = event.target
// bail if user didn't actually load a file
if (R.isNil(input.files[0])) {
return false
}
// read the file
var reader = new FileReader()
var scala_file = reader.readAsText(input.files[0])
reader.onload = function () {
// get filename
jQuery('#txt_name').val(input.files[0].name.slice(0, -4))
scala_file = reader.result
// split scala_file data into individual lines
var lines = scala_file.split(newlineTest)
// determine the first line of scala file that contains tuning data
let first_line = lines.lastIndexOf('!') + 1
jQuery('#txt_tuning_data').val(
lines
.slice(first_line)
.map((line) => line.trim())
.join(unix_newline)
)
parse_tuning_data()
}
}
// after a tun file is loaded, this function will be called
function parse_imported_anamark_tun(event) {
// Note: this is not an AnaMark TUN v2.00 compliant parser! It is incomplete!
// At the very least, this parser should support cents-based TUN files generated by Scale Workshop & Scala.
// If anybody wants full TUN v2.00 support, send a pull request
// Have you read the TUN spec recently?
// https://www.mark-henning.de/files/am/Tuning_File_V2_Doc.pdf
var input = event.target
// bail if user didn't actually load a file
if (R.isNil(input.files[0])) {
return false
}
// read the file
var reader = new FileReader()
var tun_file = reader.readAsText(input.files[0])
reader.onload = function () {
tun_file = reader.result
// split tun_file data into individual lines
var lines = tun_file.split(newlineTest)
// get tuning name
var name = false
for (let i = 0; i < lines.length; i++) {
// Check if line is start of [Info] section
if (!name && lines[i].includes('[Info]')) {
// file has [Info] section so we expect to see a name too
name = true
}
// We saw an [Info] section during a previous loop so now we're looking for the name
else {
if (lines[i].trim().startsWith('Name')) {
// the current line contains the name
var regex = /"(.*?)"/g
name = lines[i].match(regex)[0].replace(/"/g, '').replace(/\.tun/g, '')
break
}
}
}
// If a name couldn't be found within the file, then just grab it from the filename
if (name === true || name === false) {
console.log("this shouldn't be happening right now")
name = input.files[0].name.slice(0, -4)
}
// determine if tun file contains 'Functional Tuning' block and get line number where tuning starts
var has_functional_tuning = false
var first_line = lines.findIndex(
(line) => line.includes('[Functional Tuning]') || line.includes('[Functional tuning]')
)
if (first_line === -1) {
first_line = 0
} else {
first_line += 1
has_functional_tuning = true
}
// it's best to work from the Functional Tuning if available, since it works much like a Scala scale
if (has_functional_tuning) {
jQuery('#txt_name').val(name)
var tuning = []
// get note values
for (let i = first_line; i < lines.length; i++) {
var n = i - first_line // note number
if (lines[i].includes('#=0')) {
tuning[n] = lines[i].substring(lines[i].indexOf('#=0') + 6, lines[i].length - 2).trim()
}
if (lines[i].includes('#>')) {
var m = (n + 1).toString()
var prefix = 'note ' + m + '="#>-' + m
tuning[n] = lines[i].replace(prefix, '')
tuning[n] = tuning[n].substring(3, tuning[n].indexOf('~')).trim()
}
}
jQuery('#txt_tuning_data').val(tuning.join(unix_newline))
// get base MIDI note and base frequency
for (let i = first_line + 1; i < lines.length; i++) {
if (lines[i].includes('!')) {
jQuery('#txt_base_frequency').val(
lines[i].substring(lines[i].indexOf('!') + 2, lines[i].length - 2)
)
jQuery('#txt_base_midi_note').val(
lines[i].substring(0, lines[i].indexOf('!') - 2).replace('note ', '')
)
}
}
parse_tuning_data()
return true
}
// if there's no functional tuning
else {
alert('This looks like a v0 or v1 tun file, which is not currently supported.')
return false
// RIP my willpower
/*
alert("Warning: You have imported an older v0 or v1 .TUN file with no [Functional Tuning] data. Scale Workshop will attempt to pull in all 128 notes.");
var first_line = 0;
// determine on which line of the tun file that tuning data starts, with preference for 'Exact Tuning' block, followed by 'Tuning' block.
for ( let i = 0; i < lines.length; i++ ) {
if ( lines[i].includes("[Exact Tuning]") ) {
has_functional_tuning = true;
first_line = i + 1;
break;
}
}
if ( first_line == 0 ) {
for ( let i = 0; i < lines.length; i++ ) {
if ( lines[i].includes("[Tuning]") ) {
has_functional_tuning = true;
first_line = i + 1;
break;
}
}
}
// this is where things get messy
// enter tuning data
var offset = parseFloat( lines[first_line].replace("note 0=", "") ).toFixed(6); // offset will ensure that note 0 is 1/1
let tuning_data_str;
for ( let i = first_line; i < first_line+128; i++ ) {
var n = i - first_line; // n = note number
var line = lines[i].replace( "note " + n.toString() + "=", "" ).trim();
line = parseFloat( line ).toFixed(6);
line = (parseFloat(line) + parseFloat(offset)).toFixed(6);
if ( n == 0 ) {
// clear scale field
tuning_data_str = ''
}
else if ( n == 1 ) {
tuning_data_str += line ;
}
else {
tuning_data_str += unix_newline + line;
}
}
jQuery( "#txt_tuning_data" ).val(tuning_data_str)
jQuery( "#txt_base_frequency" ).val( 440 / cents_to_decimal(offset) );
jQuery( "#txt_base_midi_note" ).val( 0 );
*/
}
}
}
function parseImportedMnlgtun(event) {
// TODO: Add features for extracting scale information to reduce a table of 128 values into a periodic scale
const input = event.target
// bail if no file was uploaded
if (R.isNil(input.files[0])) {
return false
}
const zip = new JSZip()
zip.loadAsync(input.files[0]).then((result) => {
// check if meets size requirements
const bin = result.files[Object.keys(result.files)[0]]
if (bin.name.endsWith('_bin') && bin._data.uncompressedSize % 3 === 0) {
const binaryString = bin._data.compressedContent.reduce(
(a, b) => a + String.fromCharCode(b),
''
)
// extract tuning data
let cents = mnlgBinaryToCents(binaryString)
if (cents.length > 0) {
const octaveFormat = cents.length === MNLG_OCTAVESIZE
// fix octave overflow
if (octaveFormat)
cents = cents.map((c) => (c > MNLG_OCTAVESIZE * 200 ? c - MNLG_MAXCENTS : c))
// to always play at the right frequencies, we have to use the first midi note
let octaveExp = octaveFormat ? cents[0] - 900 : cents[0] - MNLG_A_REF.val
// derive frequency from A440
const baseFrequency = MNLG_A_REF.freq * 2 ** (octaveExp / 1200)
// normalize so scale starts on unison
if (octaveFormat) {
let negCents = Math.min(...cents)
if (negCents < 0) cents = cents.map((c) => c - negCents)
} else {
cents = cents.map((c) => c - cents[0])
}
// remove unison (but may have other unison lines to preserve mapping)
cents.shift()
// add period
if (octaveFormat) cents.push(1200)
jQuery('#txt_tuning_data').val(cents.map((c) => c.toFixed(6)).join(newline))
jQuery('#txt_base_frequency').val(baseFrequency)
jQuery('#txt_base_midi_note').val(octaveFormat ? 60 : 0)
jQuery('#txt_name').val(input.files[0].name.slice(0, -9))
if (parse_tuning_data()) return true
else {
alert('Data parsing failed.')
return false
}
} else {
alert('File does not contain tuning data.')
return false
}
} else {
alert('Not a valid mnlgtun file.')
return false
}
})
}
================================================
FILE: src/js/state/actions-dom.js
================================================
// DOM changes, need to sync with state
jQuery('#input_range_main_vol').on('input', function () {
state.set('main volume', parseFloat(jQuery(this).val()))
})
jQuery('#velocity-toggler').on('click', () => {
state.set('midi velocity sensing', !state.get('midi velocity sensing'))
})
// hide virtual keyboard when mobile hamburger menu button is clicked
jQuery('button.navbar-toggle').on('click', () => {
state.set('virtual keyboard visible', false)
state.set('mobile menu visible', !state.get('mobile menu visible'))
})
// Touch keyboard (#nav_play) option clicked
jQuery('#nav_play, #launch-kbd').on('click', (event) => {
event.preventDefault()
state.set('virtual keyboard visible', !state.get('virtual keyboard visible'))
})
================================================
FILE: src/js/state/actions.js
================================================
// non-DOM changes, need to sync with state
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
state.set('virtual keyboard visible', false)
}
})
================================================
FILE: src/js/state/on-ready.js
================================================
// when all event hooks set up the initial change events can fire
state.ready()
================================================
FILE: src/js/state/reactions-dom.js
================================================
// data changed, sync it with the DOM
state.on('main volume', (value) => {
jQuery('#input_range_main_vol').val(value)
})
state.on('midi velocity sensing', (value) => {
const velocityToggleBtn = jQuery('#velocity-toggler')
if (value) {
velocityToggleBtn.removeClass('btn-basic').addClass('btn-success').text('velocity: on')
} else {
velocityToggleBtn.removeClass('btn-success').addClass('btn-basic').text('velocity: off')
}
})
state.on('virtual keyboard visible', (value) => {
if (value) {
touch_kbd_open()
} else {
touch_kbd_close()
}
})
state.on('mobile menu visible', (value) => {
if (value) {
jQuery('#mobile-menu').show()
} else {
jQuery('#mobile-menu').hide()
}
})
state.on('midi modal visible', (value, ...args) => {
const midiModal = document.getElementById('modal_midi_settings')
if (value) {
renderMidiInputsTo(midiModal.querySelector('.inputs'))
renderMidiOutputsTo(midiModal.querySelector('.outputs'))
renderMidiSettingsTo(midiModal.querySelector('.settings'))
}
})
state.on('midi enabled', (value) => {
if (value) {
jQuery('#midi-enabler')
.prop('disabled', true)
.removeClass('btn-danger')
.addClass('btn-success')
.text('on')
}
})
================================================
FILE: src/js/state/reactions.js
================================================
// data changed, handle programmatic reaction - no DOM changes
state.on('main volume', (newValue) => {
synth.setMainVolume(newValue)
})
================================================
FILE: src/js/state/state.js
================================================
class State extends EventEmitter {
constructor(initialData = {}) {
super()
this.data = initialData
}
get(key) {
return this.data[key]
}
set(key, newValue, forceEmit = false) {
const oldValue = this.data[key]
if (oldValue !== newValue) {
this.data[key] = newValue
this.emit(key, newValue, oldValue)
} else {
if (forceEmit) {
this.emit(key, newValue, oldValue)
}
}
}
ready() {
Object.entries(this.data).forEach(([key, value]) => {
this.emit(key, value)
})
}
}
const state = new State({
'main volume': 0.8,
'midi enabled': false,
'midi velocity sensing': true,
'virtual keyboard visible': false,
'mobile menu visible': false,
'midi modal visible': false
})
================================================
FILE: src/js/synth/Delay.js
================================================
class Delay {
constructor(synth) {
this.time = 0.3
this.gain = 0.4
this.inited = false
this.synth = synth
}
enable() {
if (this.inited) {
this.panL.connect(this.synth.masterGain)
this.panR.connect(this.synth.masterGain)
}
}
disable() {
if (this.inited) {
this.panL.disconnect(this.synth.masterGain)
this.panR.disconnect(this.synth.masterGain)
}
}
init(audioCtx) {
if (!this.inited) {
this.inited = true
this.channelL = audioCtx.createDelay(5.0)
this.channelR = audioCtx.createDelay(5.0)
this.gainL = audioCtx.createGain(0.8)
this.gainR = audioCtx.createGain(0.8)
// this.lowpassL = audioCtx.createBiquadFilter()
// this.lowpassR = audioCtx.createBiquadFilter()
// this.highpassL = audioCtx.createBiquadFilter()
// this.highpassR = audioCtx.createBiquadFilter()
this.panL = audioCtx.createPanner()
this.panR = audioCtx.createPanner()
// feedback loop with gain stage
this.channelL.connect(this.gainL)
this.gainL.connect(this.channelR)
this.channelR.connect(this.gainR)
this.gainR.connect(this.channelL)
// filters
// this.gainL.connect( this.lowpassL );
// this.gainR.connect( this.lowpassR );
// this.lowpassL.frequency.value = 6500;
// this.lowpassR.frequency.value = 7000;
// this.lowpassL.Q.value = 0.7;
// this.lowpassR.Q.value = 0.7;
// this.lowpassL.type = 'lowpass';
// this.lowpassR.type = 'lowpass';
// this.lowpassL.connect( this.highpassL );
// this.lowpassR.connect( this.highpassR );
// this.highpassL.frequency.value = 130;
// this.highpassR.frequency.value = 140;
// this.highpassL.Q.value = 0.7;
// this.highpassR.Q.value = 0.7;
// this.highpassL.type = 'highpass';
// this.highpassR.type = 'highpass';
// this.highpassL.connect( this.panL );
// this.highpassR.connect( this.panR );
// panning
this.gainL.connect(this.panL) // if you uncomment the above filters lines, then comment out this line
this.gainR.connect(this.panR) // if you uncomment the above filters lines, then comment out this line
this.panL.setPosition(-1, 0, 0)
this.panR.setPosition(1, 0, 0)
// setup delay time and gain for delay lines
const now = synth.now()
this.channelL.delayTime.setValueAtTime(this.time, now)
this.channelR.delayTime.setValueAtTime(this.time, now)
this.gainL.gain.setValueAtTime(this.gain, now)
this.gainR.gain.setValueAtTime(this.gain, now)
// check on init if user has already enabled delay
if (jQuery('#input_checkbox_delay_on').is(':checked')) {
this.enable()
}
}
}
}
================================================
FILE: src/js/synth/Synth.js
================================================
class Synth {
constructor() {
this.keymap = Keymap.EN
this.isomorphicMapping = {
vertical: 5, // how many scale degrees as you move up/down by rows
horizontal: 1 // how many scale degrees as you move left/right by cols
}
this.voices = []
this.midinotes_to_voices = {} // polyphonic voice allocation
this.voices_to_midinotes = {} // polyphonic voice allocation
this.polyphony = !R.isNil(localStorage.getItem('max_polyphony'))
? localStorage.getItem('max_polyphony')
: 16
this.nextVoice = 0
this.waveform = 'semisine'
this.mainVolume = 0.8
this.inited = false
this.delay = new Delay(this)
}
init() {
// only init once
if (!this.inited) {
this.inited = true
// set up Web Audio API context
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
// set up custom waveforms
this.custom_waveforms = {
warm1: this.audioCtx.createPeriodicWave(
new Float32Array([0, 10, 2, 2, 2, 1, 1, 0.5]),
new Float32Array([0, 0, 0, 0, 0, 0, 0, 0])
),
warm2: this.audioCtx.createPeriodicWave(
new Float32Array([0, 10, 5, 3.33, 2, 1]),
new Float32Array([0, 0, 0, 0, 0, 0])
),
warm3: this.audioCtx.createPeriodicWave(
new Float32Array([0, 10, 5, 5, 3]),
new Float32Array([0, 0, 0, 0, 0])
),
warm4: this.audioCtx.createPeriodicWave(
new Float32Array([0, 10, 2, 2, 1]),
new Float32Array([0, 0, 0, 0, 0])
),
octaver: this.audioCtx.createPeriodicWave(
new Float32Array([0, 1000, 500, 0, 333, 0, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 166]),
new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
),
brightness: this.audioCtx.createPeriodicWave(
new Float32Array([
0, 10, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 0.75, 0.5, 0.2, 0.1
]),
new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
),
harmonicbell: this.audioCtx.createPeriodicWave(
new Float32Array([0, 10, 2, 2, 2, 2, 0, 0, 0, 0, 0, 7]),
new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
),
template: this.audioCtx.createPeriodicWave(
// first element is DC offset, second element is fundamental, third element is 2nd harmonic, etc.
new Float32Array([0, 1, 0.5, 0.333, 0.25, 0.2, 0.167]), // sine components
new Float32Array([0, 0, 0.0, 0.0, 0.0, 0.0, 0.0]) // cosine components
)
}
// DC-blocked semisine
const semisineSineComponents = new Float32Array(64);
const semisineCosineComponents = new Float32Array(64);
for (let n = 1; n < 64; ++n) {
semisineCosineComponents[n] = 1 / (1 - 4*n*n);
}
this.custom_waveforms.semisine = this.audioCtx.createPeriodicWave(
semisineSineComponents,
semisineCosineComponents
);
// set up master gain
this.masterGain = this.audioCtx.createGain()
this.masterGain.gain.value = this.mainVolume
// set up master filter
this.masterLPfilter = this.audioCtx.createBiquadFilter()
this.masterLPfilter.frequency.value = 5000
this.masterLPfilter.Q.value = 1
this.masterLPfilter.type = 'lowpass'
// connect master gain control > filter > master output
this.masterGain.connect(this.masterLPfilter)
this.masterLPfilter.connect(this.audioCtx.destination)
// init delay
this.delay.init(this.audioCtx)
// init free running oscillators
for (i = 0; i < this.polyphony; i++) {
this.voices[i] = new Voice(this.audioCtx)
this.voices[i].bindDelay(this.delay)
this.voices[i].bindSynth(this)
this.voices[i].init()
}
}
}
setMainVolume(newValue) {
const oldValue = this.mainVolume
if (newValue !== oldValue) {
this.mainVolume = newValue
if (this.inited) {
const now = this.now()
this.masterGain.gain.value = newValue
this.masterGain.gain.setValueAtTime(newValue, now)
}
}
}
noteOn(midinote, velocity = 127) {
const frequency = tuning_table.freq[midinote]
if (!R.isNil(frequency)) {
midi.playFrequency(frequency)
// make sure note triggers only on first input (prevent duplicate notes)
if (R.isNil(this.midinotes_to_voices[midinote])) {
this.init()
// round robin voice allocation, but skip voices that are still being held
for (i = this.nextVoice; i < this.nextVoice + this.polyphony; i++) {
// if next voice is free, use it
if (R.isNil(this.voices_to_midinotes[(i + 1) % this.polyphony])) {
this.nextVoice = (i + 1) % this.polyphony
break
}
// if no free voices are found when the loop ends, voice stealing will result
}
// keep track of allocated voices
this.midinotes_to_voices[midinote] = this.nextVoice
this.voices_to_midinotes[this.nextVoice] = midinote
// trigger note start
this.voices[this.midinotes_to_voices[midinote]].start(frequency, velocity)
// indicate playing note
jQuery('#tuning-table-row-' + midinote).addClass('bg-playnote')
console.log(this.midinotes_to_voices)
//console.log("Play note " + midinote + " (" + frequency.toFixed(3) + " Hz) velocity " + velocity);
}
}
}
noteOff(midinote) {
midi.stopFrequency()
if (!R.isNil(this.midinotes_to_voices[midinote])) {
// release the note
this.voices[this.midinotes_to_voices[midinote]].stop()
// voice allocation
delete this.voices_to_midinotes[this.midinotes_to_voices[midinote]]
delete this.midinotes_to_voices[midinote]
// indicate stopped note
jQuery('#tuning-table-row-' + midinote).removeClass('bg-playnote')
if (Object.values(this.midinotes_to_voices).length) {
console.log(this.midinotes_to_voices)
}
}
}
now() {
return this.audioCtx.currentTime
}
// this function stops all active voices and cuts the delay
panic() {
// show which voices are active (playing)
console.log(this.voices)
// loop through active voices
for (let i = 0; i < this.polyphony; i++) {
// turn off voice
this.noteOff(this.voices_to_midinotes[i])
}
// turn down delay gain
jQuery('#input_range_feedback_gain').val(0)
this.delay.gain = 0
const now = this.now()
this.delay.gainL.gain.setValueAtTime(this.delay.gain, now)
this.delay.gainR.gain.setValueAtTime(this.delay.gain, now)
midi.stopFrequency()
}
}
================================================
FILE: src/js/synth/Voice.js
================================================
const getEnvelopeByName = (name) => {
const envelope = {
attackTime: 0,
decayTime: 0,
sustain: 1,
releaseTime: 0
}
switch (name) {
case 'organ':
envelope.attackTime = 0.01
envelope.decayTime = 0.3
envelope.sustain = 0.8
envelope.releaseTime = 0.01
break
case 'pad':
envelope.attackTime = 1
envelope.decayTime = 3
envelope.sustain = 0.5
envelope.releaseTime = 0.7
break
case 'perc-short':
envelope.attackTime = 0.005
envelope.decayTime = 0.3
envelope.sustain = 0.00001
envelope.releaseTime = 0.05
break
case 'perc-medium':
envelope.attackTime = 0.005
envelope.decayTime = 1.5
envelope.sustain = 0.00001
envelope.releaseTime = 0.25
break
case 'perc-long':
envelope.attackTime = 0.01
envelope.decayTime = 8
envelope.sustain = 0.00001
envelope.releaseTime = 0.8
break
}
return envelope
}
const getEnvelopeName = () => jQuery('#input_select_synth_amp_env').val()
// https://github.com/mohayonao/pseudo-audio-param/blob/master/lib/expr.js#L3
function getLinearRampToValueAtTime(t, v0, v1, t0, t1) {
var a
if (t <= t0) {
return v0
}
if (t1 <= t) {
return v1
}
a = (t - t0) / (t1 - t0)
return v0 + a * (v1 - v0)
}
// https://github.com/mohayonao/pseudo-audio-param/blob/master/lib/expr.js#L18
function getExponentialRampToValueAtTime(t, v0, v1, t0, t1) {
var a
if (t <= t0) {
return v0
}
if (t1 <= t) {
return v1
}
if (v0 === v1) {
return v0
}
a = (t - t0) / (t1 - t0)
if ((0 < v0 && 0 < v1) || (v0 < 0 && v1 < 0)) {
return v0 * Math.pow(v1 / v0, a)
}
return 0
}
const interpolateValueAtTime = (minValue, maxValue, envelope, t) => {
// interpolate attack
if (envelope.attackTime > t) {
return getLinearRampToValueAtTime(t, minValue, maxValue, 0, envelope.attackTime)
}
// interpolate decay
if (envelope.attackTime + envelope.decayTime > t) {
return getExponentialRampToValueAtTime(
t,
maxValue,
maxValue * envelope.sustain,
envelope.attackTime,
envelope.attackTime + envelope.decayTime
)
}
// interpolate sustain
return maxValue * envelope.sustain
}
class Voice {
constructor(audioCtx) {
// set up oscillator
this.vco = audioCtx.createOscillator()
// set up amplitude envelope generator
this.vca = audioCtx.createGain()
}
init() {
// timing
const now = this.synth.now()
this.vca.gain.setValueAtTime(0, now)
// routing
this.vco.connect(this.vca)
this.vco.start()
this.vca.connect(this.delay.channelL)
this.vca.connect(this.synth.masterGain)
}
start(frequency, velocity) {
// start timing
const now = this.synth.now()
this.vca.gain._startTime = now
// tune oscillator to correct frequency
this.vco.frequency.setValueAtTime(frequency, now)
// set oscillator waveform
switch (this.synth.waveform) {
case 'warm1':
this.vco.setPeriodicWave(synth.custom_waveforms.warm1)
break
case 'warm2':
this.vco.setPeriodicWave(synth.custom_waveforms.warm2)
break
case 'warm3':
this.vco.setPeriodicWave(synth.custom_waveforms.warm3)
break
case 'warm4':
this.vco.setPeriodicWave(synth.custom_waveforms.warm4)
break
case 'octaver':
this.vco.setPeriodicWave(synth.custom_waveforms.octaver)
break
case 'brightness':
this.vco.setPeriodicWave(synth.custom_waveforms.brightness)
break
case 'harmonicbell':
this.vco.setPeriodicWave(synth.custom_waveforms.harmonicbell)
break
case 'semisine':
this.vco.setPeriodicWave(synth.custom_waveforms.semisine)
break
default:
this.vco.type = this.synth.waveform
}
// get target gain
if (velocity === 0) {
// in exponentialRampToValueAtTime, target gain can't be 0
this.targetGain = 0.00001
} else {
// use velocity to determine target gain
this.targetGain = velocity = (0.2 * velocity) / 127
}
// get and set amplitude envelope
const envelope = getEnvelopeByName(getEnvelopeName())
this.attackTime = envelope.attackTime
this.decayTime = envelope.decayTime
this.sustain = envelope.sustain
this.releaseTime = envelope.releaseTime
// Attack
this.cancelEnvelope(this.vca.gain, now)
this.vca.gain.setValueAtTime(0, now)
this.vca.gain.linearRampToValueAtTime(this.targetGain, now + this.attackTime)
// Decay & Sustain
this.vca.gain.exponentialRampToValueAtTime(
this.targetGain * this.sustain,
now + this.attackTime + this.decayTime
)
}
stop() {
// timing
const now = this.synth.now()
// Release
this.cancelEnvelope(this.vca.gain, now)
this.vca.gain.setTargetAtTime(0.0, now, this.releaseTime)
}
// cancels any scheduled envelope changes in a given property's value
cancelEnvelope(property, now) {
// Firefox and Safari do not support cancelAndHoldAtTime
if (isFunction(property.cancelAndHoldAtTime)) {
property.cancelAndHoldAtTime(now)
} else {
property.cancelScheduledValues(now)
property.setValueAtTime(
interpolateValueAtTime(
0.00001,
this.targetGain,
getEnvelopeByName(getEnvelopeName()),
now - property._startTime
),
now
)
}
}
bindSynth(synth) {
this.synth = synth
}
bindDelay(delay) {
this.delay = delay
}
}
================================================
FILE: src/js/synth.js
================================================
/**
* synth.js
* Web audio synth
*/
const synth = new Synth()
// keycode_to_midinote()
// it turns a keycode to a MIDI note based on this reference layout:
//
// 1 2 3 4 5 6 7 8 9 0 - =
// Q W E R T Y U I O P [ ]
// A S D F G H J K L ; ' \
// Z X C V B N M , . /
//
function keycode_to_midinote(keycode) {
// get row/col vals from the keymap
var key = synth.keymap[keycode]
// return false if there is no note assigned to this key
if (R.isNil(key)) {
return false
}
var [row, col] = key
return (
row * synth.isomorphicMapping.vertical +
col * synth.isomorphicMapping.horizontal +
tuning_table['base_midi_note']
)
}
function touch_to_midinote([row, col]) {
if (R.isNil(row) || R.isNil(col)) {
return false
}
return (
row * synth.isomorphicMapping.vertical +
col * synth.isomorphicMapping.horizontal +
tuning_table['base_midi_note']
)
}
// is_qwerty_active()
// check if qwerty key playing should be active
// returns true if focus is in safe area for typing
// returns false if focus is on an input or textarea element
function is_qwerty_active() {
jQuery('div#qwerty-indicator').empty()
var focus = document.activeElement.tagName
if (focus == 'TEXTAREA' || focus == 'INPUT') {
jQuery('div#qwerty-indicator').html(
'
',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false,
viewport: {
selector: 'body',
padding: 0
}
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
this.inState = { click: false, hover: false, focus: false }
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
}
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
}
if (self.tip().hasClass('in') || self.hoverState == 'in') {
self.hoverState = 'in'
return
}
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.isInStateTrue = function () {
for (var key in this.inState) {
if (this.inState[key]) return true
}
return false
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
$(obj.currentTarget).data('bs.' + this.type, self)
}
if (obj instanceof $.Event) {
self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
}
if (self.isInStateTrue()) return
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
var that = this
var $tip = this.tip()
var tipId = this.getUID(this.type)
this.setContent()
$tip.attr('id', tipId)
this.$element.attr('aria-describedby', tipId)
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
this.$element.trigger('inserted.bs.' + this.type)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var orgPlacement = placement
var viewportDim = this.getPosition(this.$viewport)
placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' :
placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' :
placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' :
placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
var complete = function () {
var prevHoverState = that.hoverState
that.$element.trigger('shown.bs.' + that.type)
that.hoverState = null
if (prevHoverState == 'out') that.leave(that)
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top += marginTop
offset.left += marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({
using: function (props) {
$tip.css({
top: Math.round(props.top),
left: Math.round(props.left)
})
}
}, offset), 0)
$tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
offset.top = offset.top + height - actualHeight
}
var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
if (delta.left) offset.left += delta.left
else offset.top += delta.top
var isVertical = /top|bottom/.test(placement)
var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
$tip.offset(offset)
this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
this.arrow()
.css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
.css(isVertical ? 'top' : 'left', '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function (callback) {
var that = this
var $tip = $(this.$tip)
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
}
callback && callback()
}
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
$tip.removeClass('in')
$.support.transition && $tip.hasClass('fade') ?
$tip
.one('bsTransitionEnd', complete)
.emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
complete()
this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getPosition = function ($element) {
$element = $element || this.$element
var el = $element[0]
var isBody = el.tagName == 'BODY'
var elRect = el.getBoundingClientRect()
if (elRect.width == null) {
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
}
var isSvg = window.SVGElement && el instanceof window.SVGElement
// Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.
// See https://github.com/twbs/bootstrap/issues/20280
var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
return $.extend({}, elRect, scroll, outerDims, elOffset)
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
var delta = { top: 0, left: 0 }
if (!this.$viewport) return delta
var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
var viewportDimensions = this.getPosition(this.$viewport)
if (/right|left/.test(placement)) {
var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
if (topEdgeOffset < viewportDimensions.top) { // top overflow
delta.top = viewportDimensions.top - topEdgeOffset
} else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
}
} else {
var leftEdgeOffset = pos.left - viewportPadding
var rightEdgeOffset = pos.left + viewportPadding + actualWidth
if (leftEdgeOffset < viewportDimensions.left) { // left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
} else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
}
}
return delta
}
Tooltip.prototype.getTitle = function () {
var title
var $e = this.$element
var o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
return title
}
Tooltip.prototype.getUID = function (prefix) {
do prefix += ~~(Math.random() * 1000000)
while (document.getElementById(prefix))
return prefix
}
Tooltip.prototype.tip = function () {
if (!this.$tip) {
this.$tip = $(this.options.template)
if (this.$tip.length != 1) {
throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')
}
}
return this.$tip
}
Tooltip.prototype.arrow = function () {
return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (e) {
var self = this
if (e) {
self = $(e.currentTarget).data('bs.' + this.type)
if (!self) {
self = new this.constructor(e.currentTarget, this.getDelegateOptions())
$(e.currentTarget).data('bs.' + this.type, self)
}
}
if (e) {
self.inState.click = !self.inState.click
if (self.isInStateTrue()) self.enter(self)
else self.leave(self)
} else {
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
}
Tooltip.prototype.destroy = function () {
var that = this
clearTimeout(this.timeout)
this.hide(function () {
that.$element.off('.' + that.type).removeData('bs.' + that.type)
if (that.$tip) {
that.$tip.detach()
}
that.$tip = null
that.$arrow = null
that.$viewport = null
that.$element = null
})
}
// TOOLTIP PLUGIN DEFINITION
// =========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.tooltip
$.fn.tooltip = Plugin
$.fn.tooltip.Constructor = Tooltip
// TOOLTIP NO CONFLICT
// ===================
$.fn.tooltip.noConflict = function () {
$.fn.tooltip = old
return this
}
}(jQuery);
/* ========================================================================
* Bootstrap: popover.js v3.3.7
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// POPOVER PUBLIC CLASS DEFINITION
// ===============================
var Popover = function (element, options) {
this.init('popover', element, options)
}
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.VERSION = '3.3.7'
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
trigger: 'click',
content: '',
template: '
'
})
// NOTE: POPOVER EXTENDS tooltip.js
// ================================
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
Popover.prototype.constructor = Popover
Popover.prototype.getDefaults = function () {
return Popover.DEFAULTS
}
Popover.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
](content)
$tip.removeClass('fade top bottom left right in')
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
}
Popover.prototype.hasContent = function () {
return this.getTitle() || this.getContent()
}
Popover.prototype.getContent = function () {
var $e = this.$element
var o = this.options
return $e.attr('data-content')
|| (typeof o.content == 'function' ?
o.content.call($e[0]) :
o.content)
}
Popover.prototype.arrow = function () {
return (this.$arrow = this.$arrow || this.tip().find('.arrow'))
}
// POPOVER PLUGIN DEFINITION
// =========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && /destroy|hide/.test(option)) return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.popover
$.fn.popover = Plugin
$.fn.popover.Constructor = Popover
// POPOVER NO CONFLICT
// ===================
$.fn.popover.noConflict = function () {
$.fn.popover = old
return this
}
}(jQuery);
/* ========================================================================
* Bootstrap: scrollspy.js v3.3.7
* http://getbootstrap.com/javascript/#scrollspy
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// SCROLLSPY CLASS DEFINITION
// ==========================
function ScrollSpy(element, options) {
this.$body = $(document.body)
this.$scrollElement = $(element).is(document.body) ? $(window) : $(element)
this.options = $.extend({}, ScrollSpy.DEFAULTS, options)
this.selector = (this.options.target || '') + ' .nav li > a'
this.offsets = []
this.targets = []
this.activeTarget = null
this.scrollHeight = 0
this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this))
this.refresh()
this.process()
}
ScrollSpy.VERSION = '3.3.7'
ScrollSpy.DEFAULTS = {
offset: 10
}
ScrollSpy.prototype.getScrollHeight = function () {
return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
}
ScrollSpy.prototype.refresh = function () {
var that = this
var offsetMethod = 'offset'
var offsetBase = 0
this.offsets = []
this.targets = []
this.scrollHeight = this.getScrollHeight()
if (!$.isWindow(this.$scrollElement[0])) {
offsetMethod = 'position'
offsetBase = this.$scrollElement.scrollTop()
}
this.$body
.find(this.selector)
.map(function () {
var $el = $(this)
var href = $el.data('target') || $el.attr('href')
var $href = /^#./.test(href) && $(href)
return ($href
&& $href.length
&& $href.is(':visible')
&& [[$href[offsetMethod]().top + offsetBase, href]]) || null
})
.sort(function (a, b) { return a[0] - b[0] })
.each(function () {
that.offsets.push(this[0])
that.targets.push(this[1])
})
}
ScrollSpy.prototype.process = function () {
var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
var scrollHeight = this.getScrollHeight()
var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height()
var offsets = this.offsets
var targets = this.targets
var activeTarget = this.activeTarget
var i
if (this.scrollHeight != scrollHeight) {
this.refresh()
}
if (scrollTop >= maxScroll) {
return activeTarget != (i = targets[targets.length - 1]) && this.activate(i)
}
if (activeTarget && scrollTop < offsets[0]) {
this.activeTarget = null
return this.clear()
}
for (i = offsets.length; i--;) {
activeTarget != targets[i]
&& scrollTop >= offsets[i]
&& (offsets[i + 1] === undefined || scrollTop < offsets[i + 1])
&& this.activate(targets[i])
}
}
ScrollSpy.prototype.activate = function (target) {
this.activeTarget = target
this.clear()
var selector = this.selector +
'[data-target="' + target + '"],' +
this.selector + '[href="' + target + '"]'
var active = $(selector)
.parents('li')
.addClass('active')
if (active.parent('.dropdown-menu').length) {
active = active
.closest('li.dropdown')
.addClass('active')
}
active.trigger('activate.bs.scrollspy')
}
ScrollSpy.prototype.clear = function () {
$(this.selector)
.parentsUntil(this.options.target, '.active')
.removeClass('active')
}
// SCROLLSPY PLUGIN DEFINITION
// ===========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.scrollspy')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.scrollspy
$.fn.scrollspy = Plugin
$.fn.scrollspy.Constructor = ScrollSpy
// SCROLLSPY NO CONFLICT
// =====================
$.fn.scrollspy.noConflict = function () {
$.fn.scrollspy = old
return this
}
// SCROLLSPY DATA-API
// ==================
$(window).on('load.bs.scrollspy.data-api', function () {
$('[data-spy="scroll"]').each(function () {
var $spy = $(this)
Plugin.call($spy, $spy.data())
})
})
}(jQuery);
/* ========================================================================
* Bootstrap: tab.js v3.3.7
* http://getbootstrap.com/javascript/#tabs
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TAB CLASS DEFINITION
// ====================
var Tab = function (element) {
// jscs:disable requireDollarBeforejQueryAssignment
this.element = $(element)
// jscs:enable requireDollarBeforejQueryAssignment
}
Tab.VERSION = '3.3.7'
Tab.TRANSITION_DURATION = 150
Tab.prototype.show = function () {
var $this = this.element
var $ul = $this.closest('ul:not(.dropdown-menu)')
var selector = $this.data('target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
if ($this.parent('li').hasClass('active')) return
var $previous = $ul.find('.active:last a')
var hideEvent = $.Event('hide.bs.tab', {
relatedTarget: $this[0]
})
var showEvent = $.Event('show.bs.tab', {
relatedTarget: $previous[0]
})
$previous.trigger(hideEvent)
$this.trigger(showEvent)
if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return
var $target = $(selector)
this.activate($this.closest('li'), $ul)
this.activate($target, $target.parent(), function () {
$previous.trigger({
type: 'hidden.bs.tab',
relatedTarget: $this[0]
})
$this.trigger({
type: 'shown.bs.tab',
relatedTarget: $previous[0]
})
})
}
Tab.prototype.activate = function (element, container, callback) {
var $active = container.find('> .active')
var transition = callback
&& $.support.transition
&& ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)
function next() {
$active
.removeClass('active')
.find('> .dropdown-menu > .active')
.removeClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', false)
element
.addClass('active')
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
if (transition) {
element[0].offsetWidth // reflow for transition
element.addClass('in')
} else {
element.removeClass('fade')
}
if (element.parent('.dropdown-menu').length) {
element
.closest('li.dropdown')
.addClass('active')
.end()
.find('[data-toggle="tab"]')
.attr('aria-expanded', true)
}
callback && callback()
}
$active.length && transition ?
$active
.one('bsTransitionEnd', next)
.emulateTransitionEnd(Tab.TRANSITION_DURATION) :
next()
$active.removeClass('in')
}
// TAB PLUGIN DEFINITION
// =====================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tab')
if (!data) $this.data('bs.tab', (data = new Tab(this)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.tab
$.fn.tab = Plugin
$.fn.tab.Constructor = Tab
// TAB NO CONFLICT
// ===============
$.fn.tab.noConflict = function () {
$.fn.tab = old
return this
}
// TAB DATA-API
// ============
var clickHandler = function (e) {
e.preventDefault()
Plugin.call($(this), 'show')
}
$(document)
.on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler)
.on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler)
}(jQuery);
/* ========================================================================
* Bootstrap: affix.js v3.3.7
* http://getbootstrap.com/javascript/#affix
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// AFFIX CLASS DEFINITION
// ======================
var Affix = function (element, options) {
this.options = $.extend({}, Affix.DEFAULTS, options)
this.$target = $(this.options.target)
.on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
.on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this))
this.$element = $(element)
this.affixed = null
this.unpin = null
this.pinnedOffset = null
this.checkPosition()
}
Affix.VERSION = '3.3.7'
Affix.RESET = 'affix affix-top affix-bottom'
Affix.DEFAULTS = {
offset: 0,
target: window
}
Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) {
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
var targetHeight = this.$target.height()
if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false
if (this.affixed == 'bottom') {
if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom'
return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom'
}
var initializing = this.affixed == null
var colliderTop = initializing ? scrollTop : position.top
var colliderHeight = initializing ? targetHeight : height
if (offsetTop != null && scrollTop <= offsetTop) return 'top'
if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'
return false
}
Affix.prototype.getPinnedOffset = function () {
if (this.pinnedOffset) return this.pinnedOffset
this.$element.removeClass(Affix.RESET).addClass('affix')
var scrollTop = this.$target.scrollTop()
var position = this.$element.offset()
return (this.pinnedOffset = position.top - scrollTop)
}
Affix.prototype.checkPositionWithEventLoop = function () {
setTimeout($.proxy(this.checkPosition, this), 1)
}
Affix.prototype.checkPosition = function () {
if (!this.$element.is(':visible')) return
var height = this.$element.height()
var offset = this.options.offset
var offsetTop = offset.top
var offsetBottom = offset.bottom
var scrollHeight = Math.max($(document).height(), $(document.body).height())
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element)
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element)
var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom)
if (this.affixed != affix) {
if (this.unpin != null) this.$element.css('top', '')
var affixType = 'affix' + (affix ? '-' + affix : '')
var e = $.Event(affixType + '.bs.affix')
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
this.affixed = affix
this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null
this.$element
.removeClass(Affix.RESET)
.addClass(affixType)
.trigger(affixType.replace('affix', 'affixed') + '.bs.affix')
}
if (affix == 'bottom') {
this.$element.offset({
top: scrollHeight - height - offsetBottom
})
}
}
// AFFIX PLUGIN DEFINITION
// =======================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.affix')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.affix
$.fn.affix = Plugin
$.fn.affix.Constructor = Affix
// AFFIX NO CONFLICT
// =================
$.fn.affix.noConflict = function () {
$.fn.affix = old
return this
}
// AFFIX DATA-API
// ==============
$(window).on('load', function () {
$('[data-spy="affix"]').each(function () {
var $spy = $(this)
var data = $spy.data()
data.offset = data.offset || {}
if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom
if (data.offsetTop != null) data.offset.top = data.offsetTop
Plugin.call($spy, data)
})
})
}(jQuery);
================================================
FILE: src/lib/bootstrap-3.3.7-dist/js/npm.js
================================================
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
require('../../js/transition.js')
require('../../js/alert.js')
require('../../js/button.js')
require('../../js/carousel.js')
require('../../js/collapse.js')
require('../../js/dropdown.js')
require('../../js/modal.js')
require('../../js/tooltip.js')
require('../../js/popover.js')
require('../../js/scrollspy.js')
require('../../js/tab.js')
require('../../js/affix.js')
================================================
FILE: src/lib/decimal.js
================================================
;(function (globalScope) {
'use strict';
/*
* decimal.js v10.3.1
* An arbitrary-precision Decimal type for JavaScript.
* https://github.com/MikeMcl/decimal.js
* Copyright (c) 2021 Michael Mclaughlin
* MIT Licence
*/
// ----------------------------------- EDITABLE DEFAULTS ------------------------------------ //
// The maximum exponent magnitude.
// The limit on the value of `toExpNeg`, `toExpPos`, `minE` and `maxE`.
var EXP_LIMIT = 9e15, // 0 to 9e15
// The limit on the value of `precision`, and on the value of the first argument to
// `toDecimalPlaces`, `toExponential`, `toFixed`, `toPrecision` and `toSignificantDigits`.
MAX_DIGITS = 1e9, // 0 to 1e9
// Base conversion alphabet.
NUMERALS = '0123456789abcdef',
// The natural logarithm of 10 (1025 digits).
LN10 = '2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058',
// Pi (1025 digits).
PI = '3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789',
// The initial configuration properties of the Decimal constructor.
DEFAULTS = {
// These values must be integers within the stated ranges (inclusive).
// Most of these values can be changed at run-time using the `Decimal.config` method.
// The maximum number of significant digits of the result of a calculation or base conversion.
// E.g. `Decimal.config({ precision: 20 });`
precision: 20, // 1 to MAX_DIGITS
// The rounding mode used when rounding to `precision`.
//
// ROUND_UP 0 Away from zero.
// ROUND_DOWN 1 Towards zero.
// ROUND_CEIL 2 Towards +Infinity.
// ROUND_FLOOR 3 Towards -Infinity.
// ROUND_HALF_UP 4 Towards nearest neighbour. If equidistant, up.
// ROUND_HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.
// ROUND_HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.
// ROUND_HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.
// ROUND_HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.
//
// E.g.
// `Decimal.rounding = 4;`
// `Decimal.rounding = Decimal.ROUND_HALF_UP;`
rounding: 4, // 0 to 8
// The modulo mode used when calculating the modulus: a mod n.
// The quotient (q = a / n) is calculated according to the corresponding rounding mode.
// The remainder (r) is calculated as: r = a - n * q.
//
// UP 0 The remainder is positive if the dividend is negative, else is negative.
// DOWN 1 The remainder has the same sign as the dividend (JavaScript %).
// FLOOR 3 The remainder has the same sign as the divisor (Python %).
// HALF_EVEN 6 The IEEE 754 remainder function.
// EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). Always positive.
//
// Truncated division (1), floored division (3), the IEEE 754 remainder (6), and Euclidian
// division (9) are commonly used for the modulus operation. The other rounding modes can also
// be used, but they may not give useful results.
modulo: 1, // 0 to 9
// The exponent value at and beneath which `toString` returns exponential notation.
// JavaScript numbers: -7
toExpNeg: -7, // 0 to -EXP_LIMIT
// The exponent value at and above which `toString` returns exponential notation.
// JavaScript numbers: 21
toExpPos: 21, // 0 to EXP_LIMIT
// The minimum exponent value, beneath which underflow to zero occurs.
// JavaScript numbers: -324 (5e-324)
minE: -EXP_LIMIT, // -1 to -EXP_LIMIT
// The maximum exponent value, above which overflow to Infinity occurs.
// JavaScript numbers: 308 (1.7976931348623157e+308)
maxE: EXP_LIMIT, // 1 to EXP_LIMIT
// Whether to use cryptographically-secure random number generation, if available.
crypto: false // true/false
},
// ----------------------------------- END OF EDITABLE DEFAULTS ------------------------------- //
Decimal, inexact, noConflict, quadrant,
external = true,
decimalError = '[DecimalError] ',
invalidArgument = decimalError + 'Invalid argument: ',
precisionLimitExceeded = decimalError + 'Precision limit exceeded',
cryptoUnavailable = decimalError + 'crypto unavailable',
tag = '[object Decimal]',
mathfloor = Math.floor,
mathpow = Math.pow,
isBinary = /^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,
isHex = /^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,
isOctal = /^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,
isDecimal = /^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,
BASE = 1e7,
LOG_BASE = 7,
MAX_SAFE_INTEGER = 9007199254740991,
LN10_PRECISION = LN10.length - 1,
PI_PRECISION = PI.length - 1,
// Decimal.prototype object
P = { toStringTag: tag };
// Decimal prototype methods
/*
* absoluteValue abs
* ceil
* clampedTo clamp
* comparedTo cmp
* cosine cos
* cubeRoot cbrt
* decimalPlaces dp
* dividedBy div
* dividedToIntegerBy divToInt
* equals eq
* floor
* greaterThan gt
* greaterThanOrEqualTo gte
* hyperbolicCosine cosh
* hyperbolicSine sinh
* hyperbolicTangent tanh
* inverseCosine acos
* inverseHyperbolicCosine acosh
* inverseHyperbolicSine asinh
* inverseHyperbolicTangent atanh
* inverseSine asin
* inverseTangent atan
* isFinite
* isInteger isInt
* isNaN
* isNegative isNeg
* isPositive isPos
* isZero
* lessThan lt
* lessThanOrEqualTo lte
* logarithm log
* [maximum] [max]
* [minimum] [min]
* minus sub
* modulo mod
* naturalExponential exp
* naturalLogarithm ln
* negated neg
* plus add
* precision sd
* round
* sine sin
* squareRoot sqrt
* tangent tan
* times mul
* toBinary
* toDecimalPlaces toDP
* toExponential
* toFixed
* toFraction
* toHexadecimal toHex
* toNearest
* toNumber
* toOctal
* toPower pow
* toPrecision
* toSignificantDigits toSD
* toString
* truncated trunc
* valueOf toJSON
*/
/*
* Return a new Decimal whose value is the absolute value of this Decimal.
*
*/
P.absoluteValue = P.abs = function () {
var x = new this.constructor(this);
if (x.s < 0) x.s = 1;
return finalise(x);
};
/*
* Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the
* direction of positive Infinity.
*
*/
P.ceil = function () {
return finalise(new this.constructor(this), this.e + 1, 2);
};
/*
* Return a new Decimal whose value is the value of this Decimal clamped to the range
* delineated by `min` and `max`.
*
* min {number|string|Decimal}
* max {number|string|Decimal}
*
*/
P.clampedTo = P.clamp = function (min, max) {
var k,
x = this,
Ctor = x.constructor;
min = new Ctor(min);
max = new Ctor(max);
if (!min.s || !max.s) return new Ctor(NaN);
if (min.gt(max)) throw Error(invalidArgument + max);
k = x.cmp(min);
return k < 0 ? min : x.cmp(max) > 0 ? max : new Ctor(x);
};
/*
* Return
* 1 if the value of this Decimal is greater than the value of `y`,
* -1 if the value of this Decimal is less than the value of `y`,
* 0 if they have the same value,
* NaN if the value of either Decimal is NaN.
*
*/
P.comparedTo = P.cmp = function (y) {
var i, j, xdL, ydL,
x = this,
xd = x.d,
yd = (y = new x.constructor(y)).d,
xs = x.s,
ys = y.s;
// Either NaN or ±Infinity?
if (!xd || !yd) {
return !xs || !ys ? NaN : xs !== ys ? xs : xd === yd ? 0 : !xd ^ xs < 0 ? 1 : -1;
}
// Either zero?
if (!xd[0] || !yd[0]) return xd[0] ? xs : yd[0] ? -ys : 0;
// Signs differ?
if (xs !== ys) return xs;
// Compare exponents.
if (x.e !== y.e) return x.e > y.e ^ xs < 0 ? 1 : -1;
xdL = xd.length;
ydL = yd.length;
// Compare digit by digit.
for (i = 0, j = xdL < ydL ? xdL : ydL; i < j; ++i) {
if (xd[i] !== yd[i]) return xd[i] > yd[i] ^ xs < 0 ? 1 : -1;
}
// Compare lengths.
return xdL === ydL ? 0 : xdL > ydL ^ xs < 0 ? 1 : -1;
};
/*
* Return a new Decimal whose value is the cosine of the value in radians of this Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-1, 1]
*
* cos(0) = 1
* cos(-0) = 1
* cos(Infinity) = NaN
* cos(-Infinity) = NaN
* cos(NaN) = NaN
*
*/
P.cosine = P.cos = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (!x.d) return new Ctor(NaN);
// cos(0) = cos(-0) = 1
if (!x.d[0]) return new Ctor(1);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;
Ctor.rounding = 1;
x = cosine(Ctor, toLessThanHalfPi(Ctor, x));
Ctor.precision = pr;
Ctor.rounding = rm;
return finalise(quadrant == 2 || quadrant == 3 ? x.neg() : x, pr, rm, true);
};
/*
*
* Return a new Decimal whose value is the cube root of the value of this Decimal, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
* cbrt(0) = 0
* cbrt(-0) = -0
* cbrt(1) = 1
* cbrt(-1) = -1
* cbrt(N) = N
* cbrt(-I) = -I
* cbrt(I) = I
*
* Math.cbrt(x) = (x < 0 ? -Math.pow(-x, 1/3) : Math.pow(x, 1/3))
*
*/
P.cubeRoot = P.cbrt = function () {
var e, m, n, r, rep, s, sd, t, t3, t3plusx,
x = this,
Ctor = x.constructor;
if (!x.isFinite() || x.isZero()) return new Ctor(x);
external = false;
// Initial estimate.
s = x.s * mathpow(x.s * x, 1 / 3);
// Math.cbrt underflow/overflow?
// Pass x to Math.pow as integer, then adjust the exponent of the result.
if (!s || Math.abs(s) == 1 / 0) {
n = digitsToString(x.d);
e = x.e;
// Adjust n exponent so it is a multiple of 3 away from x exponent.
if (s = (e - n.length + 1) % 3) n += (s == 1 || s == -2 ? '0' : '00');
s = mathpow(n, 1 / 3);
// Rarely, e may be one less than the result exponent value.
e = mathfloor((e + 1) / 3) - (e % 3 == (e < 0 ? -1 : 2));
if (s == 1 / 0) {
n = '5e' + e;
} else {
n = s.toExponential();
n = n.slice(0, n.indexOf('e') + 1) + e;
}
r = new Ctor(n);
r.s = x.s;
} else {
r = new Ctor(s.toString());
}
sd = (e = Ctor.precision) + 3;
// Halley's method.
// TODO? Compare Newton's method.
for (;;) {
t = r;
t3 = t.times(t).times(t);
t3plusx = t3.plus(x);
r = divide(t3plusx.plus(x).times(t), t3plusx.plus(t3), sd + 2, 1);
// TODO? Replace with for-loop and checkRoundingDigits.
if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {
n = n.slice(sd - 3, sd + 1);
// The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or 4999
// , i.e. approaching a rounding boundary, continue the iteration.
if (n == '9999' || !rep && n == '4999') {
// On the first iteration only, check to see if rounding up gives the exact result as the
// nines may infinitely repeat.
if (!rep) {
finalise(t, e + 1, 0);
if (t.times(t).times(t).eq(x)) {
r = t;
break;
}
}
sd += 4;
rep = 1;
} else {
// If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result.
// If not, then there are further digits and m will be truthy.
if (!+n || !+n.slice(1) && n.charAt(0) == '5') {
// Truncate to the first rounding digit.
finalise(r, e + 1, 1);
m = !r.times(r).times(r).eq(x);
}
break;
}
}
}
external = true;
return finalise(r, e, Ctor.rounding, m);
};
/*
* Return the number of decimal places of the value of this Decimal.
*
*/
P.decimalPlaces = P.dp = function () {
var w,
d = this.d,
n = NaN;
if (d) {
w = d.length - 1;
n = (w - mathfloor(this.e / LOG_BASE)) * LOG_BASE;
// Subtract the number of trailing zeros of the last word.
w = d[w];
if (w) for (; w % 10 == 0; w /= 10) n--;
if (n < 0) n = 0;
}
return n;
};
/*
* n / 0 = I
* n / N = N
* n / I = 0
* 0 / n = 0
* 0 / 0 = N
* 0 / N = N
* 0 / I = 0
* N / n = N
* N / 0 = N
* N / N = N
* N / I = N
* I / n = I
* I / 0 = I
* I / N = N
* I / I = N
*
* Return a new Decimal whose value is the value of this Decimal divided by `y`, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
*/
P.dividedBy = P.div = function (y) {
return divide(this, new this.constructor(y));
};
/*
* Return a new Decimal whose value is the integer part of dividing the value of this Decimal
* by the value of `y`, rounded to `precision` significant digits using rounding mode `rounding`.
*
*/
P.dividedToIntegerBy = P.divToInt = function (y) {
var x = this,
Ctor = x.constructor;
return finalise(divide(x, new Ctor(y), 0, 1, 1), Ctor.precision, Ctor.rounding);
};
/*
* Return true if the value of this Decimal is equal to the value of `y`, otherwise return false.
*
*/
P.equals = P.eq = function (y) {
return this.cmp(y) === 0;
};
/*
* Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the
* direction of negative Infinity.
*
*/
P.floor = function () {
return finalise(new this.constructor(this), this.e + 1, 3);
};
/*
* Return true if the value of this Decimal is greater than the value of `y`, otherwise return
* false.
*
*/
P.greaterThan = P.gt = function (y) {
return this.cmp(y) > 0;
};
/*
* Return true if the value of this Decimal is greater than or equal to the value of `y`,
* otherwise return false.
*
*/
P.greaterThanOrEqualTo = P.gte = function (y) {
var k = this.cmp(y);
return k == 1 || k === 0;
};
/*
* Return a new Decimal whose value is the hyperbolic cosine of the value in radians of this
* Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [1, Infinity]
*
* cosh(x) = 1 + x^2/2! + x^4/4! + x^6/6! + ...
*
* cosh(0) = 1
* cosh(-0) = 1
* cosh(Infinity) = Infinity
* cosh(-Infinity) = Infinity
* cosh(NaN) = NaN
*
* x time taken (ms) result
* 1000 9 9.8503555700852349694e+433
* 10000 25 4.4034091128314607936e+4342
* 100000 171 1.4033316802130615897e+43429
* 1000000 3817 1.5166076984010437725e+434294
* 10000000 abandoned after 2 minute wait
*
* TODO? Compare performance of cosh(x) = 0.5 * (exp(x) + exp(-x))
*
*/
P.hyperbolicCosine = P.cosh = function () {
var k, n, pr, rm, len,
x = this,
Ctor = x.constructor,
one = new Ctor(1);
if (!x.isFinite()) return new Ctor(x.s ? 1 / 0 : NaN);
if (x.isZero()) return one;
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;
Ctor.rounding = 1;
len = x.d.length;
// Argument reduction: cos(4x) = 1 - 8cos^2(x) + 8cos^4(x) + 1
// i.e. cos(x) = 1 - cos^2(x/4)(8 - 8cos^2(x/4))
// Estimate the optimum number of times to use the argument reduction.
// TODO? Estimation reused from cosine() and may not be optimal here.
if (len < 32) {
k = Math.ceil(len / 3);
n = (1 / tinyPow(4, k)).toString();
} else {
k = 16;
n = '2.3283064365386962890625e-10';
}
x = taylorSeries(Ctor, 1, x.times(n), new Ctor(1), true);
// Reverse argument reduction
var cosh2_x,
i = k,
d8 = new Ctor(8);
for (; i--;) {
cosh2_x = x.times(x);
x = one.minus(cosh2_x.times(d8.minus(cosh2_x.times(d8))));
}
return finalise(x, Ctor.precision = pr, Ctor.rounding = rm, true);
};
/*
* Return a new Decimal whose value is the hyperbolic sine of the value in radians of this
* Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-Infinity, Infinity]
*
* sinh(x) = x + x^3/3! + x^5/5! + x^7/7! + ...
*
* sinh(0) = 0
* sinh(-0) = -0
* sinh(Infinity) = Infinity
* sinh(-Infinity) = -Infinity
* sinh(NaN) = NaN
*
* x time taken (ms)
* 10 2 ms
* 100 5 ms
* 1000 14 ms
* 10000 82 ms
* 100000 886 ms 1.4033316802130615897e+43429
* 200000 2613 ms
* 300000 5407 ms
* 400000 8824 ms
* 500000 13026 ms 8.7080643612718084129e+217146
* 1000000 48543 ms
*
* TODO? Compare performance of sinh(x) = 0.5 * (exp(x) - exp(-x))
*
*/
P.hyperbolicSine = P.sinh = function () {
var k, pr, rm, len,
x = this,
Ctor = x.constructor;
if (!x.isFinite() || x.isZero()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;
Ctor.rounding = 1;
len = x.d.length;
if (len < 3) {
x = taylorSeries(Ctor, 2, x, x, true);
} else {
// Alternative argument reduction: sinh(3x) = sinh(x)(3 + 4sinh^2(x))
// i.e. sinh(x) = sinh(x/3)(3 + 4sinh^2(x/3))
// 3 multiplications and 1 addition
// Argument reduction: sinh(5x) = sinh(x)(5 + sinh^2(x)(20 + 16sinh^2(x)))
// i.e. sinh(x) = sinh(x/5)(5 + sinh^2(x/5)(20 + 16sinh^2(x/5)))
// 4 multiplications and 2 additions
// Estimate the optimum number of times to use the argument reduction.
k = 1.4 * Math.sqrt(len);
k = k > 16 ? 16 : k | 0;
x = x.times(1 / tinyPow(5, k));
x = taylorSeries(Ctor, 2, x, x, true);
// Reverse argument reduction
var sinh2_x,
d5 = new Ctor(5),
d16 = new Ctor(16),
d20 = new Ctor(20);
for (; k--;) {
sinh2_x = x.times(x);
x = x.times(d5.plus(sinh2_x.times(d16.times(sinh2_x).plus(d20))));
}
}
Ctor.precision = pr;
Ctor.rounding = rm;
return finalise(x, pr, rm, true);
};
/*
* Return a new Decimal whose value is the hyperbolic tangent of the value in radians of this
* Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-1, 1]
*
* tanh(x) = sinh(x) / cosh(x)
*
* tanh(0) = 0
* tanh(-0) = -0
* tanh(Infinity) = 1
* tanh(-Infinity) = -1
* tanh(NaN) = NaN
*
*/
P.hyperbolicTangent = P.tanh = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (!x.isFinite()) return new Ctor(x.s);
if (x.isZero()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + 7;
Ctor.rounding = 1;
return divide(x.sinh(), x.cosh(), Ctor.precision = pr, Ctor.rounding = rm);
};
/*
* Return a new Decimal whose value is the arccosine (inverse cosine) in radians of the value of
* this Decimal.
*
* Domain: [-1, 1]
* Range: [0, pi]
*
* acos(x) = pi/2 - asin(x)
*
* acos(0) = pi/2
* acos(-0) = pi/2
* acos(1) = 0
* acos(-1) = pi
* acos(1/2) = pi/3
* acos(-1/2) = 2*pi/3
* acos(|x| > 1) = NaN
* acos(NaN) = NaN
*
*/
P.inverseCosine = P.acos = function () {
var halfPi,
x = this,
Ctor = x.constructor,
k = x.abs().cmp(1),
pr = Ctor.precision,
rm = Ctor.rounding;
if (k !== -1) {
return k === 0
// |x| is 1
? x.isNeg() ? getPi(Ctor, pr, rm) : new Ctor(0)
// |x| > 1 or x is NaN
: new Ctor(NaN);
}
if (x.isZero()) return getPi(Ctor, pr + 4, rm).times(0.5);
// TODO? Special case acos(0.5) = pi/3 and acos(-0.5) = 2*pi/3
Ctor.precision = pr + 6;
Ctor.rounding = 1;
x = x.asin();
halfPi = getPi(Ctor, pr + 4, rm).times(0.5);
Ctor.precision = pr;
Ctor.rounding = rm;
return halfPi.minus(x);
};
/*
* Return a new Decimal whose value is the inverse of the hyperbolic cosine in radians of the
* value of this Decimal.
*
* Domain: [1, Infinity]
* Range: [0, Infinity]
*
* acosh(x) = ln(x + sqrt(x^2 - 1))
*
* acosh(x < 1) = NaN
* acosh(NaN) = NaN
* acosh(Infinity) = Infinity
* acosh(-Infinity) = NaN
* acosh(0) = NaN
* acosh(-0) = NaN
* acosh(1) = 0
* acosh(-1) = NaN
*
*/
P.inverseHyperbolicCosine = P.acosh = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (x.lte(1)) return new Ctor(x.eq(1) ? 0 : NaN);
if (!x.isFinite()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + Math.max(Math.abs(x.e), x.sd()) + 4;
Ctor.rounding = 1;
external = false;
x = x.times(x).minus(1).sqrt().plus(x);
external = true;
Ctor.precision = pr;
Ctor.rounding = rm;
return x.ln();
};
/*
* Return a new Decimal whose value is the inverse of the hyperbolic sine in radians of the value
* of this Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-Infinity, Infinity]
*
* asinh(x) = ln(x + sqrt(x^2 + 1))
*
* asinh(NaN) = NaN
* asinh(Infinity) = Infinity
* asinh(-Infinity) = -Infinity
* asinh(0) = 0
* asinh(-0) = -0
*
*/
P.inverseHyperbolicSine = P.asinh = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (!x.isFinite() || x.isZero()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + 2 * Math.max(Math.abs(x.e), x.sd()) + 6;
Ctor.rounding = 1;
external = false;
x = x.times(x).plus(1).sqrt().plus(x);
external = true;
Ctor.precision = pr;
Ctor.rounding = rm;
return x.ln();
};
/*
* Return a new Decimal whose value is the inverse of the hyperbolic tangent in radians of the
* value of this Decimal.
*
* Domain: [-1, 1]
* Range: [-Infinity, Infinity]
*
* atanh(x) = 0.5 * ln((1 + x) / (1 - x))
*
* atanh(|x| > 1) = NaN
* atanh(NaN) = NaN
* atanh(Infinity) = NaN
* atanh(-Infinity) = NaN
* atanh(0) = 0
* atanh(-0) = -0
* atanh(1) = Infinity
* atanh(-1) = -Infinity
*
*/
P.inverseHyperbolicTangent = P.atanh = function () {
var pr, rm, wpr, xsd,
x = this,
Ctor = x.constructor;
if (!x.isFinite()) return new Ctor(NaN);
if (x.e >= 0) return new Ctor(x.abs().eq(1) ? x.s / 0 : x.isZero() ? x : NaN);
pr = Ctor.precision;
rm = Ctor.rounding;
xsd = x.sd();
if (Math.max(xsd, pr) < 2 * -x.e - 1) return finalise(new Ctor(x), pr, rm, true);
Ctor.precision = wpr = xsd - x.e;
x = divide(x.plus(1), new Ctor(1).minus(x), wpr + pr, 1);
Ctor.precision = pr + 4;
Ctor.rounding = 1;
x = x.ln();
Ctor.precision = pr;
Ctor.rounding = rm;
return x.times(0.5);
};
/*
* Return a new Decimal whose value is the arcsine (inverse sine) in radians of the value of this
* Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-pi/2, pi/2]
*
* asin(x) = 2*atan(x/(1 + sqrt(1 - x^2)))
*
* asin(0) = 0
* asin(-0) = -0
* asin(1/2) = pi/6
* asin(-1/2) = -pi/6
* asin(1) = pi/2
* asin(-1) = -pi/2
* asin(|x| > 1) = NaN
* asin(NaN) = NaN
*
* TODO? Compare performance of Taylor series.
*
*/
P.inverseSine = P.asin = function () {
var halfPi, k,
pr, rm,
x = this,
Ctor = x.constructor;
if (x.isZero()) return new Ctor(x);
k = x.abs().cmp(1);
pr = Ctor.precision;
rm = Ctor.rounding;
if (k !== -1) {
// |x| is 1
if (k === 0) {
halfPi = getPi(Ctor, pr + 4, rm).times(0.5);
halfPi.s = x.s;
return halfPi;
}
// |x| > 1 or x is NaN
return new Ctor(NaN);
}
// TODO? Special case asin(1/2) = pi/6 and asin(-1/2) = -pi/6
Ctor.precision = pr + 6;
Ctor.rounding = 1;
x = x.div(new Ctor(1).minus(x.times(x)).sqrt().plus(1)).atan();
Ctor.precision = pr;
Ctor.rounding = rm;
return x.times(2);
};
/*
* Return a new Decimal whose value is the arctangent (inverse tangent) in radians of the value
* of this Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-pi/2, pi/2]
*
* atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...
*
* atan(0) = 0
* atan(-0) = -0
* atan(1) = pi/4
* atan(-1) = -pi/4
* atan(Infinity) = pi/2
* atan(-Infinity) = -pi/2
* atan(NaN) = NaN
*
*/
P.inverseTangent = P.atan = function () {
var i, j, k, n, px, t, r, wpr, x2,
x = this,
Ctor = x.constructor,
pr = Ctor.precision,
rm = Ctor.rounding;
if (!x.isFinite()) {
if (!x.s) return new Ctor(NaN);
if (pr + 4 <= PI_PRECISION) {
r = getPi(Ctor, pr + 4, rm).times(0.5);
r.s = x.s;
return r;
}
} else if (x.isZero()) {
return new Ctor(x);
} else if (x.abs().eq(1) && pr + 4 <= PI_PRECISION) {
r = getPi(Ctor, pr + 4, rm).times(0.25);
r.s = x.s;
return r;
}
Ctor.precision = wpr = pr + 10;
Ctor.rounding = 1;
// TODO? if (x >= 1 && pr <= PI_PRECISION) atan(x) = halfPi * x.s - atan(1 / x);
// Argument reduction
// Ensure |x| < 0.42
// atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))
k = Math.min(28, wpr / LOG_BASE + 2 | 0);
for (i = k; i; --i) x = x.div(x.times(x).plus(1).sqrt().plus(1));
external = false;
j = Math.ceil(wpr / LOG_BASE);
n = 1;
x2 = x.times(x);
r = new Ctor(x);
px = x;
// atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...
for (; i !== -1;) {
px = px.times(x2);
t = r.minus(px.div(n += 2));
px = px.times(x2);
r = t.plus(px.div(n += 2));
if (r.d[j] !== void 0) for (i = j; r.d[i] === t.d[i] && i--;);
}
if (k) r = r.times(2 << (k - 1));
external = true;
return finalise(r, Ctor.precision = pr, Ctor.rounding = rm, true);
};
/*
* Return true if the value of this Decimal is a finite number, otherwise return false.
*
*/
P.isFinite = function () {
return !!this.d;
};
/*
* Return true if the value of this Decimal is an integer, otherwise return false.
*
*/
P.isInteger = P.isInt = function () {
return !!this.d && mathfloor(this.e / LOG_BASE) > this.d.length - 2;
};
/*
* Return true if the value of this Decimal is NaN, otherwise return false.
*
*/
P.isNaN = function () {
return !this.s;
};
/*
* Return true if the value of this Decimal is negative, otherwise return false.
*
*/
P.isNegative = P.isNeg = function () {
return this.s < 0;
};
/*
* Return true if the value of this Decimal is positive, otherwise return false.
*
*/
P.isPositive = P.isPos = function () {
return this.s > 0;
};
/*
* Return true if the value of this Decimal is 0 or -0, otherwise return false.
*
*/
P.isZero = function () {
return !!this.d && this.d[0] === 0;
};
/*
* Return true if the value of this Decimal is less than `y`, otherwise return false.
*
*/
P.lessThan = P.lt = function (y) {
return this.cmp(y) < 0;
};
/*
* Return true if the value of this Decimal is less than or equal to `y`, otherwise return false.
*
*/
P.lessThanOrEqualTo = P.lte = function (y) {
return this.cmp(y) < 1;
};
/*
* Return the logarithm of the value of this Decimal to the specified base, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* If no base is specified, return log[10](arg).
*
* log[base](arg) = ln(arg) / ln(base)
*
* The result will always be correctly rounded if the base of the log is 10, and 'almost always'
* otherwise:
*
* Depending on the rounding mode, the result may be incorrectly rounded if the first fifteen
* rounding digits are [49]99999999999999 or [50]00000000000000. In that case, the maximum error
* between the result and the correctly rounded result will be one ulp (unit in the last place).
*
* log[-b](a) = NaN
* log[0](a) = NaN
* log[1](a) = NaN
* log[NaN](a) = NaN
* log[Infinity](a) = NaN
* log[b](0) = -Infinity
* log[b](-0) = -Infinity
* log[b](-a) = NaN
* log[b](1) = 0
* log[b](Infinity) = Infinity
* log[b](NaN) = NaN
*
* [base] {number|string|Decimal} The base of the logarithm.
*
*/
P.logarithm = P.log = function (base) {
var isBase10, d, denominator, k, inf, num, sd, r,
arg = this,
Ctor = arg.constructor,
pr = Ctor.precision,
rm = Ctor.rounding,
guard = 5;
// Default base is 10.
if (base == null) {
base = new Ctor(10);
isBase10 = true;
} else {
base = new Ctor(base);
d = base.d;
// Return NaN if base is negative, or non-finite, or is 0 or 1.
if (base.s < 0 || !d || !d[0] || base.eq(1)) return new Ctor(NaN);
isBase10 = base.eq(10);
}
d = arg.d;
// Is arg negative, non-finite, 0 or 1?
if (arg.s < 0 || !d || !d[0] || arg.eq(1)) {
return new Ctor(d && !d[0] ? -1 / 0 : arg.s != 1 ? NaN : d ? 0 : 1 / 0);
}
// The result will have a non-terminating decimal expansion if base is 10 and arg is not an
// integer power of 10.
if (isBase10) {
if (d.length > 1) {
inf = true;
} else {
for (k = d[0]; k % 10 === 0;) k /= 10;
inf = k !== 1;
}
}
external = false;
sd = pr + guard;
num = naturalLogarithm(arg, sd);
denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);
// The result will have 5 rounding digits.
r = divide(num, denominator, sd, 1);
// If at a rounding boundary, i.e. the result's rounding digits are [49]9999 or [50]0000,
// calculate 10 further digits.
//
// If the result is known to have an infinite decimal expansion, repeat this until it is clear
// that the result is above or below the boundary. Otherwise, if after calculating the 10
// further digits, the last 14 are nines, round up and assume the result is exact.
// Also assume the result is exact if the last 14 are zero.
//
// Example of a result that will be incorrectly rounded:
// log[1048576](4503599627370502) = 2.60000000000000009610279511444746...
// The above result correctly rounded using ROUND_CEIL to 1 decimal place should be 2.7, but it
// will be given as 2.6 as there are 15 zeros immediately after the requested decimal place, so
// the exact result would be assumed to be 2.6, which rounded using ROUND_CEIL to 1 decimal
// place is still 2.6.
if (checkRoundingDigits(r.d, k = pr, rm)) {
do {
sd += 10;
num = naturalLogarithm(arg, sd);
denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);
r = divide(num, denominator, sd, 1);
if (!inf) {
// Check for 14 nines from the 2nd rounding digit, as the first may be 4.
if (+digitsToString(r.d).slice(k + 1, k + 15) + 1 == 1e14) {
r = finalise(r, pr + 1, 0);
}
break;
}
} while (checkRoundingDigits(r.d, k += 10, rm));
}
external = true;
return finalise(r, pr, rm);
};
/*
* Return a new Decimal whose value is the maximum of the arguments and the value of this Decimal.
*
* arguments {number|string|Decimal}
*
P.max = function () {
Array.prototype.push.call(arguments, this);
return maxOrMin(this.constructor, arguments, 'lt');
};
*/
/*
* Return a new Decimal whose value is the minimum of the arguments and the value of this Decimal.
*
* arguments {number|string|Decimal}
*
P.min = function () {
Array.prototype.push.call(arguments, this);
return maxOrMin(this.constructor, arguments, 'gt');
};
*/
/*
* n - 0 = n
* n - N = N
* n - I = -I
* 0 - n = -n
* 0 - 0 = 0
* 0 - N = N
* 0 - I = -I
* N - n = N
* N - 0 = N
* N - N = N
* N - I = N
* I - n = I
* I - 0 = I
* I - N = N
* I - I = N
*
* Return a new Decimal whose value is the value of this Decimal minus `y`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
*/
P.minus = P.sub = function (y) {
var d, e, i, j, k, len, pr, rm, xd, xe, xLTy, yd,
x = this,
Ctor = x.constructor;
y = new Ctor(y);
// If either is not finite...
if (!x.d || !y.d) {
// Return NaN if either is NaN.
if (!x.s || !y.s) y = new Ctor(NaN);
// Return y negated if x is finite and y is ±Infinity.
else if (x.d) y.s = -y.s;
// Return x if y is finite and x is ±Infinity.
// Return x if both are ±Infinity with different signs.
// Return NaN if both are ±Infinity with the same sign.
else y = new Ctor(y.d || x.s !== y.s ? x : NaN);
return y;
}
// If signs differ...
if (x.s != y.s) {
y.s = -y.s;
return x.plus(y);
}
xd = x.d;
yd = y.d;
pr = Ctor.precision;
rm = Ctor.rounding;
// If either is zero...
if (!xd[0] || !yd[0]) {
// Return y negated if x is zero and y is non-zero.
if (yd[0]) y.s = -y.s;
// Return x if y is zero and x is non-zero.
else if (xd[0]) y = new Ctor(x);
// Return zero if both are zero.
// From IEEE 754 (2008) 6.3: 0 - 0 = -0 - -0 = -0 when rounding to -Infinity.
else return new Ctor(rm === 3 ? -0 : 0);
return external ? finalise(y, pr, rm) : y;
}
// x and y are finite, non-zero numbers with the same sign.
// Calculate base 1e7 exponents.
e = mathfloor(y.e / LOG_BASE);
xe = mathfloor(x.e / LOG_BASE);
xd = xd.slice();
k = xe - e;
// If base 1e7 exponents differ...
if (k) {
xLTy = k < 0;
if (xLTy) {
d = xd;
k = -k;
len = yd.length;
} else {
d = yd;
e = xe;
len = xd.length;
}
// Numbers with massively different exponents would result in a very high number of
// zeros needing to be prepended, but this can be avoided while still ensuring correct
// rounding by limiting the number of zeros to `Math.ceil(pr / LOG_BASE) + 2`.
i = Math.max(Math.ceil(pr / LOG_BASE), len) + 2;
if (k > i) {
k = i;
d.length = 1;
}
// Prepend zeros to equalise exponents.
d.reverse();
for (i = k; i--;) d.push(0);
d.reverse();
// Base 1e7 exponents equal.
} else {
// Check digits to determine which is the bigger number.
i = xd.length;
len = yd.length;
xLTy = i < len;
if (xLTy) len = i;
for (i = 0; i < len; i++) {
if (xd[i] != yd[i]) {
xLTy = xd[i] < yd[i];
break;
}
}
k = 0;
}
if (xLTy) {
d = xd;
xd = yd;
yd = d;
y.s = -y.s;
}
len = xd.length;
// Append zeros to `xd` if shorter.
// Don't add zeros to `yd` if shorter as subtraction only needs to start at `yd` length.
for (i = yd.length - len; i > 0; --i) xd[len++] = 0;
// Subtract yd from xd.
for (i = yd.length; i > k;) {
if (xd[--i] < yd[i]) {
for (j = i; j && xd[--j] === 0;) xd[j] = BASE - 1;
--xd[j];
xd[i] += BASE;
}
xd[i] -= yd[i];
}
// Remove trailing zeros.
for (; xd[--len] === 0;) xd.pop();
// Remove leading zeros and adjust exponent accordingly.
for (; xd[0] === 0; xd.shift()) --e;
// Zero?
if (!xd[0]) return new Ctor(rm === 3 ? -0 : 0);
y.d = xd;
y.e = getBase10Exponent(xd, e);
return external ? finalise(y, pr, rm) : y;
};
/*
* n % 0 = N
* n % N = N
* n % I = n
* 0 % n = 0
* -0 % n = -0
* 0 % 0 = N
* 0 % N = N
* 0 % I = 0
* N % n = N
* N % 0 = N
* N % N = N
* N % I = N
* I % n = N
* I % 0 = N
* I % N = N
* I % I = N
*
* Return a new Decimal whose value is the value of this Decimal modulo `y`, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
* The result depends on the modulo mode.
*
*/
P.modulo = P.mod = function (y) {
var q,
x = this,
Ctor = x.constructor;
y = new Ctor(y);
// Return NaN if x is ±Infinity or NaN, or y is NaN or ±0.
if (!x.d || !y.s || y.d && !y.d[0]) return new Ctor(NaN);
// Return x if y is ±Infinity or x is ±0.
if (!y.d || x.d && !x.d[0]) {
return finalise(new Ctor(x), Ctor.precision, Ctor.rounding);
}
// Prevent rounding of intermediate calculations.
external = false;
if (Ctor.modulo == 9) {
// Euclidian division: q = sign(y) * floor(x / abs(y))
// result = x - q * y where 0 <= result < abs(y)
q = divide(x, y.abs(), 0, 3, 1);
q.s *= y.s;
} else {
q = divide(x, y, 0, Ctor.modulo, 1);
}
q = q.times(y);
external = true;
return x.minus(q);
};
/*
* Return a new Decimal whose value is the natural exponential of the value of this Decimal,
* i.e. the base e raised to the power the value of this Decimal, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
*/
P.naturalExponential = P.exp = function () {
return naturalExponential(this);
};
/*
* Return a new Decimal whose value is the natural logarithm of the value of this Decimal,
* rounded to `precision` significant digits using rounding mode `rounding`.
*
*/
P.naturalLogarithm = P.ln = function () {
return naturalLogarithm(this);
};
/*
* Return a new Decimal whose value is the value of this Decimal negated, i.e. as if multiplied by
* -1.
*
*/
P.negated = P.neg = function () {
var x = new this.constructor(this);
x.s = -x.s;
return finalise(x);
};
/*
* n + 0 = n
* n + N = N
* n + I = I
* 0 + n = n
* 0 + 0 = 0
* 0 + N = N
* 0 + I = I
* N + n = N
* N + 0 = N
* N + N = N
* N + I = N
* I + n = I
* I + 0 = I
* I + N = N
* I + I = I
*
* Return a new Decimal whose value is the value of this Decimal plus `y`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
*/
P.plus = P.add = function (y) {
var carry, d, e, i, k, len, pr, rm, xd, yd,
x = this,
Ctor = x.constructor;
y = new Ctor(y);
// If either is not finite...
if (!x.d || !y.d) {
// Return NaN if either is NaN.
if (!x.s || !y.s) y = new Ctor(NaN);
// Return x if y is finite and x is ±Infinity.
// Return x if both are ±Infinity with the same sign.
// Return NaN if both are ±Infinity with different signs.
// Return y if x is finite and y is ±Infinity.
else if (!x.d) y = new Ctor(y.d || x.s === y.s ? x : NaN);
return y;
}
// If signs differ...
if (x.s != y.s) {
y.s = -y.s;
return x.minus(y);
}
xd = x.d;
yd = y.d;
pr = Ctor.precision;
rm = Ctor.rounding;
// If either is zero...
if (!xd[0] || !yd[0]) {
// Return x if y is zero.
// Return y if y is non-zero.
if (!yd[0]) y = new Ctor(x);
return external ? finalise(y, pr, rm) : y;
}
// x and y are finite, non-zero numbers with the same sign.
// Calculate base 1e7 exponents.
k = mathfloor(x.e / LOG_BASE);
e = mathfloor(y.e / LOG_BASE);
xd = xd.slice();
i = k - e;
// If base 1e7 exponents differ...
if (i) {
if (i < 0) {
d = xd;
i = -i;
len = yd.length;
} else {
d = yd;
e = k;
len = xd.length;
}
// Limit number of zeros prepended to max(ceil(pr / LOG_BASE), len) + 1.
k = Math.ceil(pr / LOG_BASE);
len = k > len ? k + 1 : len + 1;
if (i > len) {
i = len;
d.length = 1;
}
// Prepend zeros to equalise exponents. Note: Faster to use reverse then do unshifts.
d.reverse();
for (; i--;) d.push(0);
d.reverse();
}
len = xd.length;
i = yd.length;
// If yd is longer than xd, swap xd and yd so xd points to the longer array.
if (len - i < 0) {
i = len;
d = yd;
yd = xd;
xd = d;
}
// Only start adding at yd.length - 1 as the further digits of xd can be left as they are.
for (carry = 0; i;) {
carry = (xd[--i] = xd[i] + yd[i] + carry) / BASE | 0;
xd[i] %= BASE;
}
if (carry) {
xd.unshift(carry);
++e;
}
// Remove trailing zeros.
// No need to check for zero, as +x + +y != 0 && -x + -y != 0
for (len = xd.length; xd[--len] == 0;) xd.pop();
y.d = xd;
y.e = getBase10Exponent(xd, e);
return external ? finalise(y, pr, rm) : y;
};
/*
* Return the number of significant digits of the value of this Decimal.
*
* [z] {boolean|number} Whether to count integer-part trailing zeros: true, false, 1 or 0.
*
*/
P.precision = P.sd = function (z) {
var k,
x = this;
if (z !== void 0 && z !== !!z && z !== 1 && z !== 0) throw Error(invalidArgument + z);
if (x.d) {
k = getPrecision(x.d);
if (z && x.e + 1 > k) k = x.e + 1;
} else {
k = NaN;
}
return k;
};
/*
* Return a new Decimal whose value is the value of this Decimal rounded to a whole number using
* rounding mode `rounding`.
*
*/
P.round = function () {
var x = this,
Ctor = x.constructor;
return finalise(new Ctor(x), x.e + 1, Ctor.rounding);
};
/*
* Return a new Decimal whose value is the sine of the value in radians of this Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-1, 1]
*
* sin(x) = x - x^3/3! + x^5/5! - ...
*
* sin(0) = 0
* sin(-0) = -0
* sin(Infinity) = NaN
* sin(-Infinity) = NaN
* sin(NaN) = NaN
*
*/
P.sine = P.sin = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (!x.isFinite()) return new Ctor(NaN);
if (x.isZero()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;
Ctor.rounding = 1;
x = sine(Ctor, toLessThanHalfPi(Ctor, x));
Ctor.precision = pr;
Ctor.rounding = rm;
return finalise(quadrant > 2 ? x.neg() : x, pr, rm, true);
};
/*
* Return a new Decimal whose value is the square root of this Decimal, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* sqrt(-n) = N
* sqrt(N) = N
* sqrt(-I) = N
* sqrt(I) = I
* sqrt(0) = 0
* sqrt(-0) = -0
*
*/
P.squareRoot = P.sqrt = function () {
var m, n, sd, r, rep, t,
x = this,
d = x.d,
e = x.e,
s = x.s,
Ctor = x.constructor;
// Negative/NaN/Infinity/zero?
if (s !== 1 || !d || !d[0]) {
return new Ctor(!s || s < 0 && (!d || d[0]) ? NaN : d ? x : 1 / 0);
}
external = false;
// Initial estimate.
s = Math.sqrt(+x);
// Math.sqrt underflow/overflow?
// Pass x to Math.sqrt as integer, then adjust the exponent of the result.
if (s == 0 || s == 1 / 0) {
n = digitsToString(d);
if ((n.length + e) % 2 == 0) n += '0';
s = Math.sqrt(n);
e = mathfloor((e + 1) / 2) - (e < 0 || e % 2);
if (s == 1 / 0) {
n = '5e' + e;
} else {
n = s.toExponential();
n = n.slice(0, n.indexOf('e') + 1) + e;
}
r = new Ctor(n);
} else {
r = new Ctor(s.toString());
}
sd = (e = Ctor.precision) + 3;
// Newton-Raphson iteration.
for (;;) {
t = r;
r = t.plus(divide(x, t, sd + 2, 1)).times(0.5);
// TODO? Replace with for-loop and checkRoundingDigits.
if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {
n = n.slice(sd - 3, sd + 1);
// The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or
// 4999, i.e. approaching a rounding boundary, continue the iteration.
if (n == '9999' || !rep && n == '4999') {
// On the first iteration only, check to see if rounding up gives the exact result as the
// nines may infinitely repeat.
if (!rep) {
finalise(t, e + 1, 0);
if (t.times(t).eq(x)) {
r = t;
break;
}
}
sd += 4;
rep = 1;
} else {
// If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result.
// If not, then there are further digits and m will be truthy.
if (!+n || !+n.slice(1) && n.charAt(0) == '5') {
// Truncate to the first rounding digit.
finalise(r, e + 1, 1);
m = !r.times(r).eq(x);
}
break;
}
}
}
external = true;
return finalise(r, e, Ctor.rounding, m);
};
/*
* Return a new Decimal whose value is the tangent of the value in radians of this Decimal.
*
* Domain: [-Infinity, Infinity]
* Range: [-Infinity, Infinity]
*
* tan(0) = 0
* tan(-0) = -0
* tan(Infinity) = NaN
* tan(-Infinity) = NaN
* tan(NaN) = NaN
*
*/
P.tangent = P.tan = function () {
var pr, rm,
x = this,
Ctor = x.constructor;
if (!x.isFinite()) return new Ctor(NaN);
if (x.isZero()) return new Ctor(x);
pr = Ctor.precision;
rm = Ctor.rounding;
Ctor.precision = pr + 10;
Ctor.rounding = 1;
x = x.sin();
x.s = 1;
x = divide(x, new Ctor(1).minus(x.times(x)).sqrt(), pr + 10, 0);
Ctor.precision = pr;
Ctor.rounding = rm;
return finalise(quadrant == 2 || quadrant == 4 ? x.neg() : x, pr, rm, true);
};
/*
* n * 0 = 0
* n * N = N
* n * I = I
* 0 * n = 0
* 0 * 0 = 0
* 0 * N = N
* 0 * I = N
* N * n = N
* N * 0 = N
* N * N = N
* N * I = N
* I * n = I
* I * 0 = N
* I * N = N
* I * I = I
*
* Return a new Decimal whose value is this Decimal times `y`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
*/
P.times = P.mul = function (y) {
var carry, e, i, k, r, rL, t, xdL, ydL,
x = this,
Ctor = x.constructor,
xd = x.d,
yd = (y = new Ctor(y)).d;
y.s *= x.s;
// If either is NaN, ±Infinity or ±0...
if (!xd || !xd[0] || !yd || !yd[0]) {
return new Ctor(!y.s || xd && !xd[0] && !yd || yd && !yd[0] && !xd
// Return NaN if either is NaN.
// Return NaN if x is ±0 and y is ±Infinity, or y is ±0 and x is ±Infinity.
? NaN
// Return ±Infinity if either is ±Infinity.
// Return ±0 if either is ±0.
: !xd || !yd ? y.s / 0 : y.s * 0);
}
e = mathfloor(x.e / LOG_BASE) + mathfloor(y.e / LOG_BASE);
xdL = xd.length;
ydL = yd.length;
// Ensure xd points to the longer array.
if (xdL < ydL) {
r = xd;
xd = yd;
yd = r;
rL = xdL;
xdL = ydL;
ydL = rL;
}
// Initialise the result array with zeros.
r = [];
rL = xdL + ydL;
for (i = rL; i--;) r.push(0);
// Multiply!
for (i = ydL; --i >= 0;) {
carry = 0;
for (k = xdL + i; k > i;) {
t = r[k] + yd[i] * xd[k - i - 1] + carry;
r[k--] = t % BASE | 0;
carry = t / BASE | 0;
}
r[k] = (r[k] + carry) % BASE | 0;
}
// Remove trailing zeros.
for (; !r[--rL];) r.pop();
if (carry) ++e;
else r.shift();
y.d = r;
y.e = getBase10Exponent(r, e);
return external ? finalise(y, Ctor.precision, Ctor.rounding) : y;
};
/*
* Return a string representing the value of this Decimal in base 2, round to `sd` significant
* digits using rounding mode `rm`.
*
* If the optional `sd` argument is present then return binary exponential notation.
*
* [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toBinary = function (sd, rm) {
return toStringBinary(this, 2, sd, rm);
};
/*
* Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `dp`
* decimal places using rounding mode `rm` or `rounding` if `rm` is omitted.
*
* If `dp` is omitted, return a new Decimal whose value is the value of this Decimal.
*
* [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toDecimalPlaces = P.toDP = function (dp, rm) {
var x = this,
Ctor = x.constructor;
x = new Ctor(x);
if (dp === void 0) return x;
checkInt32(dp, 0, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
return finalise(x, dp + x.e + 1, rm);
};
/*
* Return a string representing the value of this Decimal in exponential notation rounded to
* `dp` fixed decimal places using rounding mode `rounding`.
*
* [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toExponential = function (dp, rm) {
var str,
x = this,
Ctor = x.constructor;
if (dp === void 0) {
str = finiteToString(x, true);
} else {
checkInt32(dp, 0, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
x = finalise(new Ctor(x), dp + 1, rm);
str = finiteToString(x, true, dp + 1);
}
return x.isNeg() && !x.isZero() ? '-' + str : str;
};
/*
* Return a string representing the value of this Decimal in normal (fixed-point) notation to
* `dp` fixed decimal places and rounded using rounding mode `rm` or `rounding` if `rm` is
* omitted.
*
* As with JavaScript numbers, (-0).toFixed(0) is '0', but e.g. (-0.00001).toFixed(0) is '-0'.
*
* [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
* (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'.
* (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'.
* (-0).toFixed(3) is '0.000'.
* (-0.5).toFixed(0) is '-0'.
*
*/
P.toFixed = function (dp, rm) {
var str, y,
x = this,
Ctor = x.constructor;
if (dp === void 0) {
str = finiteToString(x);
} else {
checkInt32(dp, 0, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
y = finalise(new Ctor(x), dp + x.e + 1, rm);
str = finiteToString(y, false, dp + y.e + 1);
}
// To determine whether to add the minus sign look at the value before it was rounded,
// i.e. look at `x` rather than `y`.
return x.isNeg() && !x.isZero() ? '-' + str : str;
};
/*
* Return an array representing the value of this Decimal as a simple fraction with an integer
* numerator and an integer denominator.
*
* The denominator will be a positive non-zero value less than or equal to the specified maximum
* denominator. If a maximum denominator is not specified, the denominator will be the lowest
* value necessary to represent the number exactly.
*
* [maxD] {number|string|Decimal} Maximum denominator. Integer >= 1 and < Infinity.
*
*/
P.toFraction = function (maxD) {
var d, d0, d1, d2, e, k, n, n0, n1, pr, q, r,
x = this,
xd = x.d,
Ctor = x.constructor;
if (!xd) return new Ctor(x);
n1 = d0 = new Ctor(1);
d1 = n0 = new Ctor(0);
d = new Ctor(d1);
e = d.e = getPrecision(xd) - x.e - 1;
k = e % LOG_BASE;
d.d[0] = mathpow(10, k < 0 ? LOG_BASE + k : k);
if (maxD == null) {
// d is 10**e, the minimum max-denominator needed.
maxD = e > 0 ? d : n1;
} else {
n = new Ctor(maxD);
if (!n.isInt() || n.lt(n1)) throw Error(invalidArgument + n);
maxD = n.gt(d) ? (e > 0 ? d : n1) : n;
}
external = false;
n = new Ctor(digitsToString(xd));
pr = Ctor.precision;
Ctor.precision = e = xd.length * LOG_BASE * 2;
for (;;) {
q = divide(n, d, 0, 1, 1);
d2 = d0.plus(q.times(d1));
if (d2.cmp(maxD) == 1) break;
d0 = d1;
d1 = d2;
d2 = n1;
n1 = n0.plus(q.times(d2));
n0 = d2;
d2 = d;
d = n.minus(q.times(d2));
n = d2;
}
d2 = divide(maxD.minus(d0), d1, 0, 1, 1);
n0 = n0.plus(d2.times(n1));
d0 = d0.plus(d2.times(d1));
n0.s = n1.s = x.s;
// Determine which fraction is closer to x, n0/d0 or n1/d1?
r = divide(n1, d1, e, 1).minus(x).abs().cmp(divide(n0, d0, e, 1).minus(x).abs()) < 1
? [n1, d1] : [n0, d0];
Ctor.precision = pr;
external = true;
return r;
};
/*
* Return a string representing the value of this Decimal in base 16, round to `sd` significant
* digits using rounding mode `rm`.
*
* If the optional `sd` argument is present then return binary exponential notation.
*
* [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toHexadecimal = P.toHex = function (sd, rm) {
return toStringBinary(this, 16, sd, rm);
};
/*
* Returns a new Decimal whose value is the nearest multiple of `y` in the direction of rounding
* mode `rm`, or `Decimal.rounding` if `rm` is omitted, to the value of this Decimal.
*
* The return value will always have the same sign as this Decimal, unless either this Decimal
* or `y` is NaN, in which case the return value will be also be NaN.
*
* The return value is not affected by the value of `precision`.
*
* y {number|string|Decimal} The magnitude to round to a multiple of.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
* 'toNearest() rounding mode not an integer: {rm}'
* 'toNearest() rounding mode out of range: {rm}'
*
*/
P.toNearest = function (y, rm) {
var x = this,
Ctor = x.constructor;
x = new Ctor(x);
if (y == null) {
// If x is not finite, return x.
if (!x.d) return x;
y = new Ctor(1);
rm = Ctor.rounding;
} else {
y = new Ctor(y);
if (rm === void 0) {
rm = Ctor.rounding;
} else {
checkInt32(rm, 0, 8);
}
// If x is not finite, return x if y is not NaN, else NaN.
if (!x.d) return y.s ? x : y;
// If y is not finite, return Infinity with the sign of x if y is Infinity, else NaN.
if (!y.d) {
if (y.s) y.s = x.s;
return y;
}
}
// If y is not zero, calculate the nearest multiple of y to x.
if (y.d[0]) {
external = false;
x = divide(x, y, 0, rm, 1).times(y);
external = true;
finalise(x);
// If y is zero, return zero with the sign of x.
} else {
y.s = x.s;
x = y;
}
return x;
};
/*
* Return the value of this Decimal converted to a number primitive.
* Zero keeps its sign.
*
*/
P.toNumber = function () {
return +this;
};
/*
* Return a string representing the value of this Decimal in base 8, round to `sd` significant
* digits using rounding mode `rm`.
*
* If the optional `sd` argument is present then return binary exponential notation.
*
* [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toOctal = function (sd, rm) {
return toStringBinary(this, 8, sd, rm);
};
/*
* Return a new Decimal whose value is the value of this Decimal raised to the power `y`, rounded
* to `precision` significant digits using rounding mode `rounding`.
*
* ECMAScript compliant.
*
* pow(x, NaN) = NaN
* pow(x, ±0) = 1
* pow(NaN, non-zero) = NaN
* pow(abs(x) > 1, +Infinity) = +Infinity
* pow(abs(x) > 1, -Infinity) = +0
* pow(abs(x) == 1, ±Infinity) = NaN
* pow(abs(x) < 1, +Infinity) = +0
* pow(abs(x) < 1, -Infinity) = +Infinity
* pow(+Infinity, y > 0) = +Infinity
* pow(+Infinity, y < 0) = +0
* pow(-Infinity, odd integer > 0) = -Infinity
* pow(-Infinity, even integer > 0) = +Infinity
* pow(-Infinity, odd integer < 0) = -0
* pow(-Infinity, even integer < 0) = +0
* pow(+0, y > 0) = +0
* pow(+0, y < 0) = +Infinity
* pow(-0, odd integer > 0) = -0
* pow(-0, even integer > 0) = +0
* pow(-0, odd integer < 0) = -Infinity
* pow(-0, even integer < 0) = +Infinity
* pow(finite x < 0, finite non-integer) = NaN
*
* For non-integer or very large exponents pow(x, y) is calculated using
*
* x^y = exp(y*ln(x))
*
* Assuming the first 15 rounding digits are each equally likely to be any digit 0-9, the
* probability of an incorrectly rounded result
* P([49]9{14} | [50]0{14}) = 2 * 0.2 * 10^-14 = 4e-15 = 1/2.5e+14
* i.e. 1 in 250,000,000,000,000
*
* If a result is incorrectly rounded the maximum error will be 1 ulp (unit in last place).
*
* y {number|string|Decimal} The power to which to raise this Decimal.
*
*/
P.toPower = P.pow = function (y) {
var e, k, pr, r, rm, s,
x = this,
Ctor = x.constructor,
yn = +(y = new Ctor(y));
// Either ±Infinity, NaN or ±0?
if (!x.d || !y.d || !x.d[0] || !y.d[0]) return new Ctor(mathpow(+x, yn));
x = new Ctor(x);
if (x.eq(1)) return x;
pr = Ctor.precision;
rm = Ctor.rounding;
if (y.eq(1)) return finalise(x, pr, rm);
// y exponent
e = mathfloor(y.e / LOG_BASE);
// If y is a small integer use the 'exponentiation by squaring' algorithm.
if (e >= y.d.length - 1 && (k = yn < 0 ? -yn : yn) <= MAX_SAFE_INTEGER) {
r = intPow(Ctor, x, k, pr);
return y.s < 0 ? new Ctor(1).div(r) : finalise(r, pr, rm);
}
s = x.s;
// if x is negative
if (s < 0) {
// if y is not an integer
if (e < y.d.length - 1) return new Ctor(NaN);
// Result is positive if x is negative and the last digit of integer y is even.
if ((y.d[e] & 1) == 0) s = 1;
// if x.eq(-1)
if (x.e == 0 && x.d[0] == 1 && x.d.length == 1) {
x.s = s;
return x;
}
}
// Estimate result exponent.
// x^y = 10^e, where e = y * log10(x)
// log10(x) = log10(x_significand) + x_exponent
// log10(x_significand) = ln(x_significand) / ln(10)
k = mathpow(+x, yn);
e = k == 0 || !isFinite(k)
? mathfloor(yn * (Math.log('0.' + digitsToString(x.d)) / Math.LN10 + x.e + 1))
: new Ctor(k + '').e;
// Exponent estimate may be incorrect e.g. x: 0.999999999999999999, y: 2.29, e: 0, r.e: -1.
// Overflow/underflow?
if (e > Ctor.maxE + 1 || e < Ctor.minE - 1) return new Ctor(e > 0 ? s / 0 : 0);
external = false;
Ctor.rounding = x.s = 1;
// Estimate the extra guard digits needed to ensure five correct rounding digits from
// naturalLogarithm(x). Example of failure without these extra digits (precision: 10):
// new Decimal(2.32456).pow('2087987436534566.46411')
// should be 1.162377823e+764914905173815, but is 1.162355823e+764914905173815
k = Math.min(12, (e + '').length);
// r = x^y = exp(y*ln(x))
r = naturalExponential(y.times(naturalLogarithm(x, pr + k)), pr);
// r may be Infinity, e.g. (0.9999999999999999).pow(-1e+40)
if (r.d) {
// Truncate to the required precision plus five rounding digits.
r = finalise(r, pr + 5, 1);
// If the rounding digits are [49]9999 or [50]0000 increase the precision by 10 and recalculate
// the result.
if (checkRoundingDigits(r.d, pr, rm)) {
e = pr + 10;
// Truncate to the increased precision plus five rounding digits.
r = finalise(naturalExponential(y.times(naturalLogarithm(x, e + k)), e), e + 5, 1);
// Check for 14 nines from the 2nd rounding digit (the first rounding digit may be 4 or 9).
if (+digitsToString(r.d).slice(pr + 1, pr + 15) + 1 == 1e14) {
r = finalise(r, pr + 1, 0);
}
}
}
r.s = s;
external = true;
Ctor.rounding = rm;
return finalise(r, pr, rm);
};
/*
* Return a string representing the value of this Decimal rounded to `sd` significant digits
* using rounding mode `rounding`.
*
* Return exponential notation if `sd` is less than the number of digits necessary to represent
* the integer part of the value in normal notation.
*
* [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
*/
P.toPrecision = function (sd, rm) {
var str,
x = this,
Ctor = x.constructor;
if (sd === void 0) {
str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
} else {
checkInt32(sd, 1, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
x = finalise(new Ctor(x), sd, rm);
str = finiteToString(x, sd <= x.e || x.e <= Ctor.toExpNeg, sd);
}
return x.isNeg() && !x.isZero() ? '-' + str : str;
};
/*
* Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `sd`
* significant digits using rounding mode `rm`, or to `precision` and `rounding` respectively if
* omitted.
*
* [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.
* [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.
*
* 'toSD() digits out of range: {sd}'
* 'toSD() digits not an integer: {sd}'
* 'toSD() rounding mode not an integer: {rm}'
* 'toSD() rounding mode out of range: {rm}'
*
*/
P.toSignificantDigits = P.toSD = function (sd, rm) {
var x = this,
Ctor = x.constructor;
if (sd === void 0) {
sd = Ctor.precision;
rm = Ctor.rounding;
} else {
checkInt32(sd, 1, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
}
return finalise(new Ctor(x), sd, rm);
};
/*
* Return a string representing the value of this Decimal.
*
* Return exponential notation if this Decimal has a positive exponent equal to or greater than
* `toExpPos`, or a negative exponent equal to or less than `toExpNeg`.
*
*/
P.toString = function () {
var x = this,
Ctor = x.constructor,
str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
return x.isNeg() && !x.isZero() ? '-' + str : str;
};
/*
* Return a new Decimal whose value is the value of this Decimal truncated to a whole number.
*
*/
P.truncated = P.trunc = function () {
return finalise(new this.constructor(this), this.e + 1, 1);
};
/*
* Return a string representing the value of this Decimal.
* Unlike `toString`, negative zero will include the minus sign.
*
*/
P.valueOf = P.toJSON = function () {
var x = this,
Ctor = x.constructor,
str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);
return x.isNeg() ? '-' + str : str;
};
// Helper functions for Decimal.prototype (P) and/or Decimal methods, and their callers.
/*
* digitsToString P.cubeRoot, P.logarithm, P.squareRoot, P.toFraction, P.toPower,
* finiteToString, naturalExponential, naturalLogarithm
* checkInt32 P.toDecimalPlaces, P.toExponential, P.toFixed, P.toNearest,
* P.toPrecision, P.toSignificantDigits, toStringBinary, random
* checkRoundingDigits P.logarithm, P.toPower, naturalExponential, naturalLogarithm
* convertBase toStringBinary, parseOther
* cos P.cos
* divide P.atanh, P.cubeRoot, P.dividedBy, P.dividedToIntegerBy,
* P.logarithm, P.modulo, P.squareRoot, P.tan, P.tanh, P.toFraction,
* P.toNearest, toStringBinary, naturalExponential, naturalLogarithm,
* taylorSeries, atan2, parseOther
* finalise P.absoluteValue, P.atan, P.atanh, P.ceil, P.cos, P.cosh,
* P.cubeRoot, P.dividedToIntegerBy, P.floor, P.logarithm, P.minus,
* P.modulo, P.negated, P.plus, P.round, P.sin, P.sinh, P.squareRoot,
* P.tan, P.times, P.toDecimalPlaces, P.toExponential, P.toFixed,
* P.toNearest, P.toPower, P.toPrecision, P.toSignificantDigits,
* P.truncated, divide, getLn10, getPi, naturalExponential,
* naturalLogarithm, ceil, floor, round, trunc
* finiteToString P.toExponential, P.toFixed, P.toPrecision, P.toString, P.valueOf,
* toStringBinary
* getBase10Exponent P.minus, P.plus, P.times, parseOther
* getLn10 P.logarithm, naturalLogarithm
* getPi P.acos, P.asin, P.atan, toLessThanHalfPi, atan2
* getPrecision P.precision, P.toFraction
* getZeroString digitsToString, finiteToString
* intPow P.toPower, parseOther
* isOdd toLessThanHalfPi
* maxOrMin max, min
* naturalExponential P.naturalExponential, P.toPower
* naturalLogarithm P.acosh, P.asinh, P.atanh, P.logarithm, P.naturalLogarithm,
* P.toPower, naturalExponential
* nonFiniteToString finiteToString, toStringBinary
* parseDecimal Decimal
* parseOther Decimal
* sin P.sin
* taylorSeries P.cosh, P.sinh, cos, sin
* toLessThanHalfPi P.cos, P.sin
* toStringBinary P.toBinary, P.toHexadecimal, P.toOctal
* truncate intPow
*
* Throws: P.logarithm, P.precision, P.toFraction, checkInt32, getLn10, getPi,
* naturalLogarithm, config, parseOther, random, Decimal
*/
function digitsToString(d) {
var i, k, ws,
indexOfLastWord = d.length - 1,
str = '',
w = d[0];
if (indexOfLastWord > 0) {
str += w;
for (i = 1; i < indexOfLastWord; i++) {
ws = d[i] + '';
k = LOG_BASE - ws.length;
if (k) str += getZeroString(k);
str += ws;
}
w = d[i];
ws = w + '';
k = LOG_BASE - ws.length;
if (k) str += getZeroString(k);
} else if (w === 0) {
return '0';
}
// Remove trailing zeros of last w.
for (; w % 10 === 0;) w /= 10;
return str + w;
}
function checkInt32(i, min, max) {
if (i !== ~~i || i < min || i > max) {
throw Error(invalidArgument + i);
}
}
/*
* Check 5 rounding digits if `repeating` is null, 4 otherwise.
* `repeating == null` if caller is `log` or `pow`,
* `repeating != null` if caller is `naturalLogarithm` or `naturalExponential`.
*/
function checkRoundingDigits(d, i, rm, repeating) {
var di, k, r, rd;
// Get the length of the first word of the array d.
for (k = d[0]; k >= 10; k /= 10) --i;
// Is the rounding digit in the first word of d?
if (--i < 0) {
i += LOG_BASE;
di = 0;
} else {
di = Math.ceil((i + 1) / LOG_BASE);
i %= LOG_BASE;
}
// i is the index (0 - 6) of the rounding digit.
// E.g. if within the word 3487563 the first rounding digit is 5,
// then i = 4, k = 1000, rd = 3487563 % 1000 = 563
k = mathpow(10, LOG_BASE - i);
rd = d[di] % k | 0;
if (repeating == null) {
if (i < 3) {
if (i == 0) rd = rd / 100 | 0;
else if (i == 1) rd = rd / 10 | 0;
r = rm < 4 && rd == 99999 || rm > 3 && rd == 49999 || rd == 50000 || rd == 0;
} else {
r = (rm < 4 && rd + 1 == k || rm > 3 && rd + 1 == k / 2) &&
(d[di + 1] / k / 100 | 0) == mathpow(10, i - 2) - 1 ||
(rd == k / 2 || rd == 0) && (d[di + 1] / k / 100 | 0) == 0;
}
} else {
if (i < 4) {
if (i == 0) rd = rd / 1000 | 0;
else if (i == 1) rd = rd / 100 | 0;
else if (i == 2) rd = rd / 10 | 0;
r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999;
} else {
r = ((repeating || rm < 4) && rd + 1 == k ||
(!repeating && rm > 3) && rd + 1 == k / 2) &&
(d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1;
}
}
return r;
}
// Convert string of `baseIn` to an array of numbers of `baseOut`.
// Eg. convertBase('255', 10, 16) returns [15, 15].
// Eg. convertBase('ff', 16, 10) returns [2, 5, 5].
function convertBase(str, baseIn, baseOut) {
var j,
arr = [0],
arrL,
i = 0,
strL = str.length;
for (; i < strL;) {
for (arrL = arr.length; arrL--;) arr[arrL] *= baseIn;
arr[0] += NUMERALS.indexOf(str.charAt(i++));
for (j = 0; j < arr.length; j++) {
if (arr[j] > baseOut - 1) {
if (arr[j + 1] === void 0) arr[j + 1] = 0;
arr[j + 1] += arr[j] / baseOut | 0;
arr[j] %= baseOut;
}
}
}
return arr.reverse();
}
/*
* cos(x) = 1 - x^2/2! + x^4/4! - ...
* |x| < pi/2
*
*/
function cosine(Ctor, x) {
var k, len, y;
if (x.isZero()) return x;
// Argument reduction: cos(4x) = 8*(cos^4(x) - cos^2(x)) + 1
// i.e. cos(x) = 8*(cos^4(x/4) - cos^2(x/4)) + 1
// Estimate the optimum number of times to use the argument reduction.
len = x.d.length;
if (len < 32) {
k = Math.ceil(len / 3);
y = (1 / tinyPow(4, k)).toString();
} else {
k = 16;
y = '2.3283064365386962890625e-10';
}
Ctor.precision += k;
x = taylorSeries(Ctor, 1, x.times(y), new Ctor(1));
// Reverse argument reduction
for (var i = k; i--;) {
var cos2x = x.times(x);
x = cos2x.times(cos2x).minus(cos2x).times(8).plus(1);
}
Ctor.precision -= k;
return x;
}
/*
* Perform division in the specified base.
*/
var divide = (function () {
// Assumes non-zero x and k, and hence non-zero result.
function multiplyInteger(x, k, base) {
var temp,
carry = 0,
i = x.length;
for (x = x.slice(); i--;) {
temp = x[i] * k + carry;
x[i] = temp % base | 0;
carry = temp / base | 0;
}
if (carry) x.unshift(carry);
return x;
}
function compare(a, b, aL, bL) {
var i, r;
if (aL != bL) {
r = aL > bL ? 1 : -1;
} else {
for (i = r = 0; i < aL; i++) {
if (a[i] != b[i]) {
r = a[i] > b[i] ? 1 : -1;
break;
}
}
}
return r;
}
function subtract(a, b, aL, base) {
var i = 0;
// Subtract b from a.
for (; aL--;) {
a[aL] -= i;
i = a[aL] < b[aL] ? 1 : 0;
a[aL] = i * base + a[aL] - b[aL];
}
// Remove leading zeros.
for (; !a[0] && a.length > 1;) a.shift();
}
return function (x, y, pr, rm, dp, base) {
var cmp, e, i, k, logBase, more, prod, prodL, q, qd, rem, remL, rem0, sd, t, xi, xL, yd0,
yL, yz,
Ctor = x.constructor,
sign = x.s == y.s ? 1 : -1,
xd = x.d,
yd = y.d;
// Either NaN, Infinity or 0?
if (!xd || !xd[0] || !yd || !yd[0]) {
return new Ctor(// Return NaN if either NaN, or both Infinity or 0.
!x.s || !y.s || (xd ? yd && xd[0] == yd[0] : !yd) ? NaN :
// Return ±0 if x is 0 or y is ±Infinity, or return ±Infinity as y is 0.
xd && xd[0] == 0 || !yd ? sign * 0 : sign / 0);
}
if (base) {
logBase = 1;
e = x.e - y.e;
} else {
base = BASE;
logBase = LOG_BASE;
e = mathfloor(x.e / logBase) - mathfloor(y.e / logBase);
}
yL = yd.length;
xL = xd.length;
q = new Ctor(sign);
qd = q.d = [];
// Result exponent may be one less than e.
// The digit array of a Decimal from toStringBinary may have trailing zeros.
for (i = 0; yd[i] == (xd[i] || 0); i++);
if (yd[i] > (xd[i] || 0)) e--;
if (pr == null) {
sd = pr = Ctor.precision;
rm = Ctor.rounding;
} else if (dp) {
sd = pr + (x.e - y.e) + 1;
} else {
sd = pr;
}
if (sd < 0) {
qd.push(1);
more = true;
} else {
// Convert precision in number of base 10 digits to base 1e7 digits.
sd = sd / logBase + 2 | 0;
i = 0;
// divisor < 1e7
if (yL == 1) {
k = 0;
yd = yd[0];
sd++;
// k is the carry.
for (; (i < xL || k) && sd--; i++) {
t = k * base + (xd[i] || 0);
qd[i] = t / yd | 0;
k = t % yd | 0;
}
more = k || i < xL;
// divisor >= 1e7
} else {
// Normalise xd and yd so highest order digit of yd is >= base/2
k = base / (yd[0] + 1) | 0;
if (k > 1) {
yd = multiplyInteger(yd, k, base);
xd = multiplyInteger(xd, k, base);
yL = yd.length;
xL = xd.length;
}
xi = yL;
rem = xd.slice(0, yL);
remL = rem.length;
// Add zeros to make remainder as long as divisor.
for (; remL < yL;) rem[remL++] = 0;
yz = yd.slice();
yz.unshift(0);
yd0 = yd[0];
if (yd[1] >= base / 2) ++yd0;
do {
k = 0;
// Compare divisor and remainder.
cmp = compare(yd, rem, yL, remL);
// If divisor < remainder.
if (cmp < 0) {
// Calculate trial digit, k.
rem0 = rem[0];
if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);
// k will be how many times the divisor goes into the current remainder.
k = rem0 / yd0 | 0;
// Algorithm:
// 1. product = divisor * trial digit (k)
// 2. if product > remainder: product -= divisor, k--
// 3. remainder -= product
// 4. if product was < remainder at 2:
// 5. compare new remainder and divisor
// 6. If remainder > divisor: remainder -= divisor, k++
if (k > 1) {
if (k >= base) k = base - 1;
// product = divisor * trial digit.
prod = multiplyInteger(yd, k, base);
prodL = prod.length;
remL = rem.length;
// Compare product and remainder.
cmp = compare(prod, rem, prodL, remL);
// product > remainder.
if (cmp == 1) {
k--;
// Subtract divisor from product.
subtract(prod, yL < prodL ? yz : yd, prodL, base);
}
} else {
// cmp is -1.
// If k is 0, there is no need to compare yd and rem again below, so change cmp to 1
// to avoid it. If k is 1 there is a need to compare yd and rem again below.
if (k == 0) cmp = k = 1;
prod = yd.slice();
}
prodL = prod.length;
if (prodL < remL) prod.unshift(0);
// Subtract product from remainder.
subtract(rem, prod, remL, base);
// If product was < previous remainder.
if (cmp == -1) {
remL = rem.length;
// Compare divisor and new remainder.
cmp = compare(yd, rem, yL, remL);
// If divisor < new remainder, subtract divisor from remainder.
if (cmp < 1) {
k++;
// Subtract divisor from remainder.
subtract(rem, yL < remL ? yz : yd, remL, base);
}
}
remL = rem.length;
} else if (cmp === 0) {
k++;
rem = [0];
} // if cmp === 1, k will be 0
// Add the next digit, k, to the result array.
qd[i++] = k;
// Update the remainder.
if (cmp && rem[0]) {
rem[remL++] = xd[xi] || 0;
} else {
rem = [xd[xi]];
remL = 1;
}
} while ((xi++ < xL || rem[0] !== void 0) && sd--);
more = rem[0] !== void 0;
}
// Leading zero?
if (!qd[0]) qd.shift();
}
// logBase is 1 when divide is being used for base conversion.
if (logBase == 1) {
q.e = e;
inexact = more;
} else {
// To calculate q.e, first get the number of digits of qd[0].
for (i = 1, k = qd[0]; k >= 10; k /= 10) i++;
q.e = i + e * logBase - 1;
finalise(q, dp ? pr + q.e + 1 : pr, rm, more);
}
return q;
};
})();
/*
* Round `x` to `sd` significant digits using rounding mode `rm`.
* Check for over/under-flow.
*/
function finalise(x, sd, rm, isTruncated) {
var digits, i, j, k, rd, roundUp, w, xd, xdi,
Ctor = x.constructor;
// Don't round if sd is null or undefined.
out: if (sd != null) {
xd = x.d;
// Infinity/NaN.
if (!xd) return x;
// rd: the rounding digit, i.e. the digit after the digit that may be rounded up.
// w: the word of xd containing rd, a base 1e7 number.
// xdi: the index of w within xd.
// digits: the number of digits of w.
// i: what would be the index of rd within w if all the numbers were 7 digits long (i.e. if
// they had leading zeros)
// j: if > 0, the actual index of rd within w (if < 0, rd is a leading zero).
// Get the length of the first word of the digits array xd.
for (digits = 1, k = xd[0]; k >= 10; k /= 10) digits++;
i = sd - digits;
// Is the rounding digit in the first word of xd?
if (i < 0) {
i += LOG_BASE;
j = sd;
w = xd[xdi = 0];
// Get the rounding digit at index j of w.
rd = w / mathpow(10, digits - j - 1) % 10 | 0;
} else {
xdi = Math.ceil((i + 1) / LOG_BASE);
k = xd.length;
if (xdi >= k) {
if (isTruncated) {
// Needed by `naturalExponential`, `naturalLogarithm` and `squareRoot`.
for (; k++ <= xdi;) xd.push(0);
w = rd = 0;
digits = 1;
i %= LOG_BASE;
j = i - LOG_BASE + 1;
} else {
break out;
}
} else {
w = k = xd[xdi];
// Get the number of digits of w.
for (digits = 1; k >= 10; k /= 10) digits++;
// Get the index of rd within w.
i %= LOG_BASE;
// Get the index of rd within w, adjusted for leading zeros.
// The number of leading zeros of w is given by LOG_BASE - digits.
j = i - LOG_BASE + digits;
// Get the rounding digit at index j of w.
rd = j < 0 ? 0 : w / mathpow(10, digits - j - 1) % 10 | 0;
}
}
// Are there any non-zero digits after the rounding digit?
isTruncated = isTruncated || sd < 0 ||
xd[xdi + 1] !== void 0 || (j < 0 ? w : w % mathpow(10, digits - j - 1));
// The expression `w % mathpow(10, digits - j - 1)` returns all the digits of w to the right
// of the digit at (left-to-right) index j, e.g. if w is 908714 and j is 2, the expression
// will give 714.
roundUp = rm < 4
? (rd || isTruncated) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))
: rd > 5 || rd == 5 && (rm == 4 || isTruncated || rm == 6 &&
// Check whether the digit to the left of the rounding digit is odd.
((i > 0 ? j > 0 ? w / mathpow(10, digits - j) : 0 : xd[xdi - 1]) % 10) & 1 ||
rm == (x.s < 0 ? 8 : 7));
if (sd < 1 || !xd[0]) {
xd.length = 0;
if (roundUp) {
// Convert sd to decimal places.
sd -= x.e + 1;
// 1, 0.1, 0.01, 0.001, 0.0001 etc.
xd[0] = mathpow(10, (LOG_BASE - sd % LOG_BASE) % LOG_BASE);
x.e = -sd || 0;
} else {
// Zero.
xd[0] = x.e = 0;
}
return x;
}
// Remove excess digits.
if (i == 0) {
xd.length = xdi;
k = 1;
xdi--;
} else {
xd.length = xdi + 1;
k = mathpow(10, LOG_BASE - i);
// E.g. 56700 becomes 56000 if 7 is the rounding digit.
// j > 0 means i > number of leading zeros of w.
xd[xdi] = j > 0 ? (w / mathpow(10, digits - j) % mathpow(10, j) | 0) * k : 0;
}
if (roundUp) {
for (;;) {
// Is the digit to be rounded up in the first word of xd?
if (xdi == 0) {
// i will be the length of xd[0] before k is added.
for (i = 1, j = xd[0]; j >= 10; j /= 10) i++;
j = xd[0] += k;
for (k = 1; j >= 10; j /= 10) k++;
// if i != k the length has increased.
if (i != k) {
x.e++;
if (xd[0] == BASE) xd[0] = 1;
}
break;
} else {
xd[xdi] += k;
if (xd[xdi] != BASE) break;
xd[xdi--] = 0;
k = 1;
}
}
}
// Remove trailing zeros.
for (i = xd.length; xd[--i] === 0;) xd.pop();
}
if (external) {
// Overflow?
if (x.e > Ctor.maxE) {
// Infinity.
x.d = null;
x.e = NaN;
// Underflow?
} else if (x.e < Ctor.minE) {
// Zero.
x.e = 0;
x.d = [0];
// Ctor.underflow = true;
} // else Ctor.underflow = false;
}
return x;
}
function finiteToString(x, isExp, sd) {
if (!x.isFinite()) return nonFiniteToString(x);
var k,
e = x.e,
str = digitsToString(x.d),
len = str.length;
if (isExp) {
if (sd && (k = sd - len) > 0) {
str = str.charAt(0) + '.' + str.slice(1) + getZeroString(k);
} else if (len > 1) {
str = str.charAt(0) + '.' + str.slice(1);
}
str = str + (x.e < 0 ? 'e' : 'e+') + x.e;
} else if (e < 0) {
str = '0.' + getZeroString(-e - 1) + str;
if (sd && (k = sd - len) > 0) str += getZeroString(k);
} else if (e >= len) {
str += getZeroString(e + 1 - len);
if (sd && (k = sd - e - 1) > 0) str = str + '.' + getZeroString(k);
} else {
if ((k = e + 1) < len) str = str.slice(0, k) + '.' + str.slice(k);
if (sd && (k = sd - len) > 0) {
if (e + 1 === len) str += '.';
str += getZeroString(k);
}
}
return str;
}
// Calculate the base 10 exponent from the base 1e7 exponent.
function getBase10Exponent(digits, e) {
var w = digits[0];
// Add the number of digits of the first word of the digits array.
for ( e *= LOG_BASE; w >= 10; w /= 10) e++;
return e;
}
function getLn10(Ctor, sd, pr) {
if (sd > LN10_PRECISION) {
// Reset global state in case the exception is caught.
external = true;
if (pr) Ctor.precision = pr;
throw Error(precisionLimitExceeded);
}
return finalise(new Ctor(LN10), sd, 1, true);
}
function getPi(Ctor, sd, rm) {
if (sd > PI_PRECISION) throw Error(precisionLimitExceeded);
return finalise(new Ctor(PI), sd, rm, true);
}
function getPrecision(digits) {
var w = digits.length - 1,
len = w * LOG_BASE + 1;
w = digits[w];
// If non-zero...
if (w) {
// Subtract the number of trailing zeros of the last word.
for (; w % 10 == 0; w /= 10) len--;
// Add the number of digits of the first word.
for (w = digits[0]; w >= 10; w /= 10) len++;
}
return len;
}
function getZeroString(k) {
var zs = '';
for (; k--;) zs += '0';
return zs;
}
/*
* Return a new Decimal whose value is the value of Decimal `x` to the power `n`, where `n` is an
* integer of type number.
*
* Implements 'exponentiation by squaring'. Called by `pow` and `parseOther`.
*
*/
function intPow(Ctor, x, n, pr) {
var isTruncated,
r = new Ctor(1),
// Max n of 9007199254740991 takes 53 loop iterations.
// Maximum digits array length; leaves [28, 34] guard digits.
k = Math.ceil(pr / LOG_BASE + 4);
external = false;
for (;;) {
if (n % 2) {
r = r.times(x);
if (truncate(r.d, k)) isTruncated = true;
}
n = mathfloor(n / 2);
if (n === 0) {
// To ensure correct rounding when r.d is truncated, increment the last word if it is zero.
n = r.d.length - 1;
if (isTruncated && r.d[n] === 0) ++r.d[n];
break;
}
x = x.times(x);
truncate(x.d, k);
}
external = true;
return r;
}
function isOdd(n) {
return n.d[n.d.length - 1] & 1;
}
/*
* Handle `max` and `min`. `ltgt` is 'lt' or 'gt'.
*/
function maxOrMin(Ctor, args, ltgt) {
var y,
x = new Ctor(args[0]),
i = 0;
for (; ++i < args.length;) {
y = new Ctor(args[i]);
if (!y.s) {
x = y;
break;
} else if (x[ltgt](y)) {
x = y;
}
}
return x;
}
/*
* Return a new Decimal whose value is the natural exponential of `x` rounded to `sd` significant
* digits.
*
* Taylor/Maclaurin series.
*
* exp(x) = x^0/0! + x^1/1! + x^2/2! + x^3/3! + ...
*
* Argument reduction:
* Repeat x = x / 32, k += 5, until |x| < 0.1
* exp(x) = exp(x / 2^k)^(2^k)
*
* Previously, the argument was initially reduced by
* exp(x) = exp(r) * 10^k where r = x - k * ln10, k = floor(x / ln10)
* to first put r in the range [0, ln10], before dividing by 32 until |x| < 0.1, but this was
* found to be slower than just dividing repeatedly by 32 as above.
*
* Max integer argument: exp('20723265836946413') = 6.3e+9000000000000000
* Min integer argument: exp('-20723265836946411') = 1.2e-9000000000000000
* (Math object integer min/max: Math.exp(709) = 8.2e+307, Math.exp(-745) = 5e-324)
*
* exp(Infinity) = Infinity
* exp(-Infinity) = 0
* exp(NaN) = NaN
* exp(±0) = 1
*
* exp(x) is non-terminating for any finite, non-zero x.
*
* The result will always be correctly rounded.
*
*/
function naturalExponential(x, sd) {
var denominator, guard, j, pow, sum, t, wpr,
rep = 0,
i = 0,
k = 0,
Ctor = x.constructor,
rm = Ctor.rounding,
pr = Ctor.precision;
// 0/NaN/Infinity?
if (!x.d || !x.d[0] || x.e > 17) {
return new Ctor(x.d
? !x.d[0] ? 1 : x.s < 0 ? 0 : 1 / 0
: x.s ? x.s < 0 ? 0 : x : 0 / 0);
}
if (sd == null) {
external = false;
wpr = pr;
} else {
wpr = sd;
}
t = new Ctor(0.03125);
// while abs(x) >= 0.1
while (x.e > -2) {
// x = x / 2^5
x = x.times(t);
k += 5;
}
// Use 2 * log10(2^k) + 5 (empirically derived) to estimate the increase in precision
// necessary to ensure the first 4 rounding digits are correct.
guard = Math.log(mathpow(2, k)) / Math.LN10 * 2 + 5 | 0;
wpr += guard;
denominator = pow = sum = new Ctor(1);
Ctor.precision = wpr;
for (;;) {
pow = finalise(pow.times(x), wpr, 1);
denominator = denominator.times(++i);
t = sum.plus(divide(pow, denominator, wpr, 1));
if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {
j = k;
while (j--) sum = finalise(sum.times(sum), wpr, 1);
// Check to see if the first 4 rounding digits are [49]999.
// If so, repeat the summation with a higher precision, otherwise
// e.g. with precision: 18, rounding: 1
// exp(18.404272462595034083567793919843761) = 98372560.1229999999 (should be 98372560.123)
// `wpr - guard` is the index of first rounding digit.
if (sd == null) {
if (rep < 3 && checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {
Ctor.precision = wpr += 10;
denominator = pow = t = new Ctor(1);
i = 0;
rep++;
} else {
return finalise(sum, Ctor.precision = pr, rm, external = true);
}
} else {
Ctor.precision = pr;
return sum;
}
}
sum = t;
}
}
/*
* Return a new Decimal whose value is the natural logarithm of `x` rounded to `sd` significant
* digits.
*
* ln(-n) = NaN
* ln(0) = -Infinity
* ln(-0) = -Infinity
* ln(1) = 0
* ln(Infinity) = Infinity
* ln(-Infinity) = NaN
* ln(NaN) = NaN
*
* ln(n) (n != 1) is non-terminating.
*
*/
function naturalLogarithm(y, sd) {
var c, c0, denominator, e, numerator, rep, sum, t, wpr, x1, x2,
n = 1,
guard = 10,
x = y,
xd = x.d,
Ctor = x.constructor,
rm = Ctor.rounding,
pr = Ctor.precision;
// Is x negative or Infinity, NaN, 0 or 1?
if (x.s < 0 || !xd || !xd[0] || !x.e && xd[0] == 1 && xd.length == 1) {
return new Ctor(xd && !xd[0] ? -1 / 0 : x.s != 1 ? NaN : xd ? 0 : x);
}
if (sd == null) {
external = false;
wpr = pr;
} else {
wpr = sd;
}
Ctor.precision = wpr += guard;
c = digitsToString(xd);
c0 = c.charAt(0);
if (Math.abs(e = x.e) < 1.5e15) {
// Argument reduction.
// The series converges faster the closer the argument is to 1, so using
// ln(a^b) = b * ln(a), ln(a) = ln(a^b) / b
// multiply the argument by itself until the leading digits of the significand are 7, 8, 9,
// 10, 11, 12 or 13, recording the number of multiplications so the sum of the series can
// later be divided by this number, then separate out the power of 10 using
// ln(a*10^b) = ln(a) + b*ln(10).
// max n is 21 (gives 0.9, 1.0 or 1.1) (9e15 / 21 = 4.2e14).
//while (c0 < 9 && c0 != 1 || c0 == 1 && c.charAt(1) > 1) {
// max n is 6 (gives 0.7 - 1.3)
while (c0 < 7 && c0 != 1 || c0 == 1 && c.charAt(1) > 3) {
x = x.times(y);
c = digitsToString(x.d);
c0 = c.charAt(0);
n++;
}
e = x.e;
if (c0 > 1) {
x = new Ctor('0.' + c);
e++;
} else {
x = new Ctor(c0 + '.' + c.slice(1));
}
} else {
// The argument reduction method above may result in overflow if the argument y is a massive
// number with exponent >= 1500000000000000 (9e15 / 6 = 1.5e15), so instead recall this
// function using ln(x*10^e) = ln(x) + e*ln(10).
t = getLn10(Ctor, wpr + 2, pr).times(e + '');
x = naturalLogarithm(new Ctor(c0 + '.' + c.slice(1)), wpr - guard).plus(t);
Ctor.precision = pr;
return sd == null ? finalise(x, pr, rm, external = true) : x;
}
// x1 is x reduced to a value near 1.
x1 = x;
// Taylor series.
// ln(y) = ln((1 + x)/(1 - x)) = 2(x + x^3/3 + x^5/5 + x^7/7 + ...)
// where x = (y - 1)/(y + 1) (|x| < 1)
sum = numerator = x = divide(x.minus(1), x.plus(1), wpr, 1);
x2 = finalise(x.times(x), wpr, 1);
denominator = 3;
for (;;) {
numerator = finalise(numerator.times(x2), wpr, 1);
t = sum.plus(divide(numerator, new Ctor(denominator), wpr, 1));
if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {
sum = sum.times(2);
// Reverse the argument reduction. Check that e is not 0 because, besides preventing an
// unnecessary calculation, -0 + 0 = +0 and to ensure correct rounding -0 needs to stay -0.
if (e !== 0) sum = sum.plus(getLn10(Ctor, wpr + 2, pr).times(e + ''));
sum = divide(sum, new Ctor(n), wpr, 1);
// Is rm > 3 and the first 4 rounding digits 4999, or rm < 4 (or the summation has
// been repeated previously) and the first 4 rounding digits 9999?
// If so, restart the summation with a higher precision, otherwise
// e.g. with precision: 12, rounding: 1
// ln(135520028.6126091714265381533) = 18.7246299999 when it should be 18.72463.
// `wpr - guard` is the index of first rounding digit.
if (sd == null) {
if (checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {
Ctor.precision = wpr += guard;
t = numerator = x = divide(x1.minus(1), x1.plus(1), wpr, 1);
x2 = finalise(x.times(x), wpr, 1);
denominator = rep = 1;
} else {
return finalise(sum, Ctor.precision = pr, rm, external = true);
}
} else {
Ctor.precision = pr;
return sum;
}
}
sum = t;
denominator += 2;
}
}
// ±Infinity, NaN.
function nonFiniteToString(x) {
// Unsigned.
return String(x.s * x.s / 0);
}
/*
* Parse the value of a new Decimal `x` from string `str`.
*/
function parseDecimal(x, str) {
var e, i, len;
// Decimal point?
if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');
// Exponential form?
if ((i = str.search(/e/i)) > 0) {
// Determine exponent.
if (e < 0) e = i;
e += +str.slice(i + 1);
str = str.substring(0, i);
} else if (e < 0) {
// Integer.
e = str.length;
}
// Determine leading zeros.
for (i = 0; str.charCodeAt(i) === 48; i++);
// Determine trailing zeros.
for (len = str.length; str.charCodeAt(len - 1) === 48; --len);
str = str.slice(i, len);
if (str) {
len -= i;
x.e = e = e - i - 1;
x.d = [];
// Transform base
// e is the base 10 exponent.
// i is where to slice str to get the first word of the digits array.
i = (e + 1) % LOG_BASE;
if (e < 0) i += LOG_BASE;
if (i < len) {
if (i) x.d.push(+str.slice(0, i));
for (len -= LOG_BASE; i < len;) x.d.push(+str.slice(i, i += LOG_BASE));
str = str.slice(i);
i = LOG_BASE - str.length;
} else {
i -= len;
}
for (; i--;) str += '0';
x.d.push(+str);
if (external) {
// Overflow?
if (x.e > x.constructor.maxE) {
// Infinity.
x.d = null;
x.e = NaN;
// Underflow?
} else if (x.e < x.constructor.minE) {
// Zero.
x.e = 0;
x.d = [0];
// x.constructor.underflow = true;
} // else x.constructor.underflow = false;
}
} else {
// Zero.
x.e = 0;
x.d = [0];
}
return x;
}
/*
* Parse the value of a new Decimal `x` from a string `str`, which is not a decimal value.
*/
function parseOther(x, str) {
var base, Ctor, divisor, i, isFloat, len, p, xd, xe;
if (str.indexOf('_') > -1) {
str = str.replace(/(\d)_(?=\d)/g, '$1');
if (isDecimal.test(str)) return parseDecimal(x, str);
} else if (str === 'Infinity' || str === 'NaN') {
if (!+str) x.s = NaN;
x.e = NaN;
x.d = null;
return x;
}
if (isHex.test(str)) {
base = 16;
str = str.toLowerCase();
} else if (isBinary.test(str)) {
base = 2;
} else if (isOctal.test(str)) {
base = 8;
} else {
throw Error(invalidArgument + str);
}
// Is there a binary exponent part?
i = str.search(/p/i);
if (i > 0) {
p = +str.slice(i + 1);
str = str.substring(2, i);
} else {
str = str.slice(2);
}
// Convert `str` as an integer then divide the result by `base` raised to a power such that the
// fraction part will be restored.
i = str.indexOf('.');
isFloat = i >= 0;
Ctor = x.constructor;
if (isFloat) {
str = str.replace('.', '');
len = str.length;
i = len - i;
// log[10](16) = 1.2041... , log[10](88) = 1.9444....
divisor = intPow(Ctor, new Ctor(base), i, i * 2);
}
xd = convertBase(str, base, BASE);
xe = xd.length - 1;
// Remove trailing zeros.
for (i = xe; xd[i] === 0; --i) xd.pop();
if (i < 0) return new Ctor(x.s * 0);
x.e = getBase10Exponent(xd, xe);
x.d = xd;
external = false;
// At what precision to perform the division to ensure exact conversion?
// maxDecimalIntegerPartDigitCount = ceil(log[10](b) * otherBaseIntegerPartDigitCount)
// log[10](2) = 0.30103, log[10](8) = 0.90309, log[10](16) = 1.20412
// E.g. ceil(1.2 * 3) = 4, so up to 4 decimal digits are needed to represent 3 hex int digits.
// maxDecimalFractionPartDigitCount = {Hex:4|Oct:3|Bin:1} * otherBaseFractionPartDigitCount
// Therefore using 4 * the number of digits of str will always be enough.
if (isFloat) x = divide(x, divisor, len * 4);
// Multiply by the binary exponent part if present.
if (p) x = x.times(Math.abs(p) < 54 ? mathpow(2, p) : Decimal.pow(2, p));
external = true;
return x;
}
/*
* sin(x) = x - x^3/3! + x^5/5! - ...
* |x| < pi/2
*
*/
function sine(Ctor, x) {
var k,
len = x.d.length;
if (len < 3) {
return x.isZero() ? x : taylorSeries(Ctor, 2, x, x);
}
// Argument reduction: sin(5x) = 16*sin^5(x) - 20*sin^3(x) + 5*sin(x)
// i.e. sin(x) = 16*sin^5(x/5) - 20*sin^3(x/5) + 5*sin(x/5)
// and sin(x) = sin(x/5)(5 + sin^2(x/5)(16sin^2(x/5) - 20))
// Estimate the optimum number of times to use the argument reduction.
k = 1.4 * Math.sqrt(len);
k = k > 16 ? 16 : k | 0;
x = x.times(1 / tinyPow(5, k));
x = taylorSeries(Ctor, 2, x, x);
// Reverse argument reduction
var sin2_x,
d5 = new Ctor(5),
d16 = new Ctor(16),
d20 = new Ctor(20);
for (; k--;) {
sin2_x = x.times(x);
x = x.times(d5.plus(sin2_x.times(d16.times(sin2_x).minus(d20))));
}
return x;
}
// Calculate Taylor series for `cos`, `cosh`, `sin` and `sinh`.
function taylorSeries(Ctor, n, x, y, isHyperbolic) {
var j, t, u, x2,
i = 1,
pr = Ctor.precision,
k = Math.ceil(pr / LOG_BASE);
external = false;
x2 = x.times(x);
u = new Ctor(y);
for (;;) {
t = divide(u.times(x2), new Ctor(n++ * n++), pr, 1);
u = isHyperbolic ? y.plus(t) : y.minus(t);
y = divide(t.times(x2), new Ctor(n++ * n++), pr, 1);
t = u.plus(y);
if (t.d[k] !== void 0) {
for (j = k; t.d[j] === u.d[j] && j--;);
if (j == -1) break;
}
j = u;
u = y;
y = t;
t = j;
i++;
}
external = true;
t.d.length = k + 1;
return t;
}
// Exponent e must be positive and non-zero.
function tinyPow(b, e) {
var n = b;
while (--e) n *= b;
return n;
}
// Return the absolute value of `x` reduced to less than or equal to half pi.
function toLessThanHalfPi(Ctor, x) {
var t,
isNeg = x.s < 0,
pi = getPi(Ctor, Ctor.precision, 1),
halfPi = pi.times(0.5);
x = x.abs();
if (x.lte(halfPi)) {
quadrant = isNeg ? 4 : 1;
return x;
}
t = x.divToInt(pi);
if (t.isZero()) {
quadrant = isNeg ? 3 : 2;
} else {
x = x.minus(t.times(pi));
// 0 <= x < pi
if (x.lte(halfPi)) {
quadrant = isOdd(t) ? (isNeg ? 2 : 3) : (isNeg ? 4 : 1);
return x;
}
quadrant = isOdd(t) ? (isNeg ? 1 : 4) : (isNeg ? 3 : 2);
}
return x.minus(pi).abs();
}
/*
* Return the value of Decimal `x` as a string in base `baseOut`.
*
* If the optional `sd` argument is present include a binary exponent suffix.
*/
function toStringBinary(x, baseOut, sd, rm) {
var base, e, i, k, len, roundUp, str, xd, y,
Ctor = x.constructor,
isExp = sd !== void 0;
if (isExp) {
checkInt32(sd, 1, MAX_DIGITS);
if (rm === void 0) rm = Ctor.rounding;
else checkInt32(rm, 0, 8);
} else {
sd = Ctor.precision;
rm = Ctor.rounding;
}
if (!x.isFinite()) {
str = nonFiniteToString(x);
} else {
str = finiteToString(x);
i = str.indexOf('.');
// Use exponential notation according to `toExpPos` and `toExpNeg`? No, but if required:
// maxBinaryExponent = floor((decimalExponent + 1) * log[2](10))
// minBinaryExponent = floor(decimalExponent * log[2](10))
// log[2](10) = 3.321928094887362347870319429489390175864
if (isExp) {
base = 2;
if (baseOut == 16) {
sd = sd * 4 - 3;
} else if (baseOut == 8) {
sd = sd * 3 - 2;
}
} else {
base = baseOut;
}
// Convert the number as an integer then divide the result by its base raised to a power such
// that the fraction part will be restored.
// Non-integer.
if (i >= 0) {
str = str.replace('.', '');
y = new Ctor(1);
y.e = str.length - i;
y.d = convertBase(finiteToString(y), 10, base);
y.e = y.d.length;
}
xd = convertBase(str, 10, base);
e = len = xd.length;
// Remove trailing zeros.
for (; xd[--len] == 0;) xd.pop();
if (!xd[0]) {
str = isExp ? '0p+0' : '0';
} else {
if (i < 0) {
e--;
} else {
x = new Ctor(x);
x.d = xd;
x.e = e;
x = divide(x, y, sd, rm, 0, base);
xd = x.d;
e = x.e;
roundUp = inexact;
}
// The rounding digit, i.e. the digit after the digit that may be rounded up.
i = xd[sd];
k = base / 2;
roundUp = roundUp || xd[sd + 1] !== void 0;
roundUp = rm < 4
? (i !== void 0 || roundUp) && (rm === 0 || rm === (x.s < 0 ? 3 : 2))
: i > k || i === k && (rm === 4 || roundUp || rm === 6 && xd[sd - 1] & 1 ||
rm === (x.s < 0 ? 8 : 7));
xd.length = sd;
if (roundUp) {
// Rounding up may mean the previous digit has to be rounded up and so on.
for (; ++xd[--sd] > base - 1;) {
xd[sd] = 0;
if (!sd) {
++e;
xd.unshift(1);
}
}
}
// Determine trailing zeros.
for (len = xd.length; !xd[len - 1]; --len);
// E.g. [4, 11, 15] becomes 4bf.
for (i = 0, str = ''; i < len; i++) str += NUMERALS.charAt(xd[i]);
// Add binary exponent suffix?
if (isExp) {
if (len > 1) {
if (baseOut == 16 || baseOut == 8) {
i = baseOut == 16 ? 4 : 3;
for (--len; len % i; len++) str += '0';
xd = convertBase(str, base, baseOut);
for (len = xd.length; !xd[len - 1]; --len);
// xd[0] will always be be 1
for (i = 1, str = '1.'; i < len; i++) str += NUMERALS.charAt(xd[i]);
} else {
str = str.charAt(0) + '.' + str.slice(1);
}
}
str = str + (e < 0 ? 'p' : 'p+') + e;
} else if (e < 0) {
for (; ++e;) str = '0' + str;
str = '0.' + str;
} else {
if (++e > len) for (e -= len; e-- ;) str += '0';
else if (e < len) str = str.slice(0, e) + '.' + str.slice(e);
}
}
str = (baseOut == 16 ? '0x' : baseOut == 2 ? '0b' : baseOut == 8 ? '0o' : '') + str;
}
return x.s < 0 ? '-' + str : str;
}
// Does not strip trailing zeros.
function truncate(arr, len) {
if (arr.length > len) {
arr.length = len;
return true;
}
}
// Decimal methods
/*
* abs
* acos
* acosh
* add
* asin
* asinh
* atan
* atanh
* atan2
* cbrt
* ceil
* clamp
* clone
* config
* cos
* cosh
* div
* exp
* floor
* hypot
* ln
* log
* log2
* log10
* max
* min
* mod
* mul
* pow
* random
* round
* set
* sign
* sin
* sinh
* sqrt
* sub
* sum
* tan
* tanh
* trunc
*/
/*
* Return a new Decimal whose value is the absolute value of `x`.
*
* x {number|string|Decimal}
*
*/
function abs(x) {
return new this(x).abs();
}
/*
* Return a new Decimal whose value is the arccosine in radians of `x`.
*
* x {number|string|Decimal}
*
*/
function acos(x) {
return new this(x).acos();
}
/*
* Return a new Decimal whose value is the inverse of the hyperbolic cosine of `x`, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function acosh(x) {
return new this(x).acosh();
}
/*
* Return a new Decimal whose value is the sum of `x` and `y`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
* y {number|string|Decimal}
*
*/
function add(x, y) {
return new this(x).plus(y);
}
/*
* Return a new Decimal whose value is the arcsine in radians of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function asin(x) {
return new this(x).asin();
}
/*
* Return a new Decimal whose value is the inverse of the hyperbolic sine of `x`, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function asinh(x) {
return new this(x).asinh();
}
/*
* Return a new Decimal whose value is the arctangent in radians of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function atan(x) {
return new this(x).atan();
}
/*
* Return a new Decimal whose value is the inverse of the hyperbolic tangent of `x`, rounded to
* `precision` significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function atanh(x) {
return new this(x).atanh();
}
/*
* Return a new Decimal whose value is the arctangent in radians of `y/x` in the range -pi to pi
* (inclusive), rounded to `precision` significant digits using rounding mode `rounding`.
*
* Domain: [-Infinity, Infinity]
* Range: [-pi, pi]
*
* y {number|string|Decimal} The y-coordinate.
* x {number|string|Decimal} The x-coordinate.
*
* atan2(±0, -0) = ±pi
* atan2(±0, +0) = ±0
* atan2(±0, -x) = ±pi for x > 0
* atan2(±0, x) = ±0 for x > 0
* atan2(-y, ±0) = -pi/2 for y > 0
* atan2(y, ±0) = pi/2 for y > 0
* atan2(±y, -Infinity) = ±pi for finite y > 0
* atan2(±y, +Infinity) = ±0 for finite y > 0
* atan2(±Infinity, x) = ±pi/2 for finite x
* atan2(±Infinity, -Infinity) = ±3*pi/4
* atan2(±Infinity, +Infinity) = ±pi/4
* atan2(NaN, x) = NaN
* atan2(y, NaN) = NaN
*
*/
function atan2(y, x) {
y = new this(y);
x = new this(x);
var r,
pr = this.precision,
rm = this.rounding,
wpr = pr + 4;
// Either NaN
if (!y.s || !x.s) {
r = new this(NaN);
// Both ±Infinity
} else if (!y.d && !x.d) {
r = getPi(this, wpr, 1).times(x.s > 0 ? 0.25 : 0.75);
r.s = y.s;
// x is ±Infinity or y is ±0
} else if (!x.d || y.isZero()) {
r = x.s < 0 ? getPi(this, pr, rm) : new this(0);
r.s = y.s;
// y is ±Infinity or x is ±0
} else if (!y.d || x.isZero()) {
r = getPi(this, wpr, 1).times(0.5);
r.s = y.s;
// Both non-zero and finite
} else if (x.s < 0) {
this.precision = wpr;
this.rounding = 1;
r = this.atan(divide(y, x, wpr, 1));
x = getPi(this, wpr, 1);
this.precision = pr;
this.rounding = rm;
r = y.s < 0 ? r.minus(x) : r.plus(x);
} else {
r = this.atan(divide(y, x, wpr, 1));
}
return r;
}
/*
* Return a new Decimal whose value is the cube root of `x`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function cbrt(x) {
return new this(x).cbrt();
}
/*
* Return a new Decimal whose value is `x` rounded to an integer using `ROUND_CEIL`.
*
* x {number|string|Decimal}
*
*/
function ceil(x) {
return finalise(x = new this(x), x.e + 1, 2);
}
/*
* Return a new Decimal whose value is `x` clamped to the range delineated by `min` and `max`.
*
* x {number|string|Decimal}
* min {number|string|Decimal}
* max {number|string|Decimal}
*
*/
function clamp(x, min, max) {
return new this(x).clamp(min, max);
}
/*
* Configure global settings for a Decimal constructor.
*
* `obj` is an object with one or more of the following properties,
*
* precision {number}
* rounding {number}
* toExpNeg {number}
* toExpPos {number}
* maxE {number}
* minE {number}
* modulo {number}
* crypto {boolean|number}
* defaults {true}
*
* E.g. Decimal.config({ precision: 20, rounding: 4 })
*
*/
function config(obj) {
if (!obj || typeof obj !== 'object') throw Error(decimalError + 'Object expected');
var i, p, v,
useDefaults = obj.defaults === true,
ps = [
'precision', 1, MAX_DIGITS,
'rounding', 0, 8,
'toExpNeg', -EXP_LIMIT, 0,
'toExpPos', 0, EXP_LIMIT,
'maxE', 0, EXP_LIMIT,
'minE', -EXP_LIMIT, 0,
'modulo', 0, 9
];
for (i = 0; i < ps.length; i += 3) {
if (p = ps[i], useDefaults) this[p] = DEFAULTS[p];
if ((v = obj[p]) !== void 0) {
if (mathfloor(v) === v && v >= ps[i + 1] && v <= ps[i + 2]) this[p] = v;
else throw Error(invalidArgument + p + ': ' + v);
}
}
if (p = 'crypto', useDefaults) this[p] = DEFAULTS[p];
if ((v = obj[p]) !== void 0) {
if (v === true || v === false || v === 0 || v === 1) {
if (v) {
if (typeof crypto != 'undefined' && crypto &&
(crypto.getRandomValues || crypto.randomBytes)) {
this[p] = true;
} else {
throw Error(cryptoUnavailable);
}
} else {
this[p] = false;
}
} else {
throw Error(invalidArgument + p + ': ' + v);
}
}
return this;
}
/*
* Return a new Decimal whose value is the cosine of `x`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function cos(x) {
return new this(x).cos();
}
/*
* Return a new Decimal whose value is the hyperbolic cosine of `x`, rounded to precision
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function cosh(x) {
return new this(x).cosh();
}
/*
* Create and return a Decimal constructor with the same configuration properties as this Decimal
* constructor.
*
*/
function clone(obj) {
var i, p, ps;
/*
* The Decimal constructor and exported function.
* Return a new Decimal instance.
*
* v {number|string|Decimal} A numeric value.
*
*/
function Decimal(v) {
var e, i, t,
x = this;
// Decimal called without new.
if (!(x instanceof Decimal)) return new Decimal(v);
// Retain a reference to this Decimal constructor, and shadow Decimal.prototype.constructor
// which points to Object.
x.constructor = Decimal;
// Duplicate.
if (isDecimalInstance(v)) {
x.s = v.s;
if (external) {
if (!v.d || v.e > Decimal.maxE) {
// Infinity.
x.e = NaN;
x.d = null;
} else if (v.e < Decimal.minE) {
// Zero.
x.e = 0;
x.d = [0];
} else {
x.e = v.e;
x.d = v.d.slice();
}
} else {
x.e = v.e;
x.d = v.d ? v.d.slice() : v.d;
}
return;
}
t = typeof v;
if (t === 'number') {
if (v === 0) {
x.s = 1 / v < 0 ? -1 : 1;
x.e = 0;
x.d = [0];
return;
}
if (v < 0) {
v = -v;
x.s = -1;
} else {
x.s = 1;
}
// Fast path for small integers.
if (v === ~~v && v < 1e7) {
for (e = 0, i = v; i >= 10; i /= 10) e++;
if (external) {
if (e > Decimal.maxE) {
x.e = NaN;
x.d = null;
} else if (e < Decimal.minE) {
x.e = 0;
x.d = [0];
} else {
x.e = e;
x.d = [v];
}
} else {
x.e = e;
x.d = [v];
}
return;
// Infinity, NaN.
} else if (v * 0 !== 0) {
if (!v) x.s = NaN;
x.e = NaN;
x.d = null;
return;
}
return parseDecimal(x, v.toString());
} else if (t !== 'string') {
throw Error(invalidArgument + v);
}
// Minus sign?
if ((i = v.charCodeAt(0)) === 45) {
v = v.slice(1);
x.s = -1;
} else {
// Plus sign?
if (i === 43) v = v.slice(1);
x.s = 1;
}
return isDecimal.test(v) ? parseDecimal(x, v) : parseOther(x, v);
}
Decimal.prototype = P;
Decimal.ROUND_UP = 0;
Decimal.ROUND_DOWN = 1;
Decimal.ROUND_CEIL = 2;
Decimal.ROUND_FLOOR = 3;
Decimal.ROUND_HALF_UP = 4;
Decimal.ROUND_HALF_DOWN = 5;
Decimal.ROUND_HALF_EVEN = 6;
Decimal.ROUND_HALF_CEIL = 7;
Decimal.ROUND_HALF_FLOOR = 8;
Decimal.EUCLID = 9;
Decimal.config = Decimal.set = config;
Decimal.clone = clone;
Decimal.isDecimal = isDecimalInstance;
Decimal.abs = abs;
Decimal.acos = acos;
Decimal.acosh = acosh; // ES6
Decimal.add = add;
Decimal.asin = asin;
Decimal.asinh = asinh; // ES6
Decimal.atan = atan;
Decimal.atanh = atanh; // ES6
Decimal.atan2 = atan2;
Decimal.cbrt = cbrt; // ES6
Decimal.ceil = ceil;
Decimal.clamp = clamp;
Decimal.cos = cos;
Decimal.cosh = cosh; // ES6
Decimal.div = div;
Decimal.exp = exp;
Decimal.floor = floor;
Decimal.hypot = hypot; // ES6
Decimal.ln = ln;
Decimal.log = log;
Decimal.log10 = log10; // ES6
Decimal.log2 = log2; // ES6
Decimal.max = max;
Decimal.min = min;
Decimal.mod = mod;
Decimal.mul = mul;
Decimal.pow = pow;
Decimal.random = random;
Decimal.round = round;
Decimal.sign = sign; // ES6
Decimal.sin = sin;
Decimal.sinh = sinh; // ES6
Decimal.sqrt = sqrt;
Decimal.sub = sub;
Decimal.sum = sum;
Decimal.tan = tan;
Decimal.tanh = tanh; // ES6
Decimal.trunc = trunc; // ES6
if (obj === void 0) obj = {};
if (obj) {
if (obj.defaults !== true) {
ps = ['precision', 'rounding', 'toExpNeg', 'toExpPos', 'maxE', 'minE', 'modulo', 'crypto'];
for (i = 0; i < ps.length;) if (!obj.hasOwnProperty(p = ps[i++])) obj[p] = this[p];
}
}
Decimal.config(obj);
return Decimal;
}
/*
* Return a new Decimal whose value is `x` divided by `y`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
* y {number|string|Decimal}
*
*/
function div(x, y) {
return new this(x).div(y);
}
/*
* Return a new Decimal whose value is the natural exponential of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} The power to which to raise the base of the natural log.
*
*/
function exp(x) {
return new this(x).exp();
}
/*
* Return a new Decimal whose value is `x` round to an integer using `ROUND_FLOOR`.
*
* x {number|string|Decimal}
*
*/
function floor(x) {
return finalise(x = new this(x), x.e + 1, 3);
}
/*
* Return a new Decimal whose value is the square root of the sum of the squares of the arguments,
* rounded to `precision` significant digits using rounding mode `rounding`.
*
* hypot(a, b, ...) = sqrt(a^2 + b^2 + ...)
*
* arguments {number|string|Decimal}
*
*/
function hypot() {
var i, n,
t = new this(0);
external = false;
for (i = 0; i < arguments.length;) {
n = new this(arguments[i++]);
if (!n.d) {
if (n.s) {
external = true;
return new this(1 / 0);
}
t = n;
} else if (t.d) {
t = t.plus(n.times(n));
}
}
external = true;
return t.sqrt();
}
/*
* Return true if object is a Decimal instance (where Decimal is any Decimal constructor),
* otherwise return false.
*
*/
function isDecimalInstance(obj) {
return obj instanceof Decimal || obj && obj.toStringTag === tag || false;
}
/*
* Return a new Decimal whose value is the natural logarithm of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function ln(x) {
return new this(x).ln();
}
/*
* Return a new Decimal whose value is the log of `x` to the base `y`, or to base 10 if no base
* is specified, rounded to `precision` significant digits using rounding mode `rounding`.
*
* log[y](x)
*
* x {number|string|Decimal} The argument of the logarithm.
* y {number|string|Decimal} The base of the logarithm.
*
*/
function log(x, y) {
return new this(x).log(y);
}
/*
* Return a new Decimal whose value is the base 2 logarithm of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function log2(x) {
return new this(x).log(2);
}
/*
* Return a new Decimal whose value is the base 10 logarithm of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function log10(x) {
return new this(x).log(10);
}
/*
* Return a new Decimal whose value is the maximum of the arguments.
*
* arguments {number|string|Decimal}
*
*/
function max() {
return maxOrMin(this, arguments, 'lt');
}
/*
* Return a new Decimal whose value is the minimum of the arguments.
*
* arguments {number|string|Decimal}
*
*/
function min() {
return maxOrMin(this, arguments, 'gt');
}
/*
* Return a new Decimal whose value is `x` modulo `y`, rounded to `precision` significant digits
* using rounding mode `rounding`.
*
* x {number|string|Decimal}
* y {number|string|Decimal}
*
*/
function mod(x, y) {
return new this(x).mod(y);
}
/*
* Return a new Decimal whose value is `x` multiplied by `y`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
* y {number|string|Decimal}
*
*/
function mul(x, y) {
return new this(x).mul(y);
}
/*
* Return a new Decimal whose value is `x` raised to the power `y`, rounded to precision
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} The base.
* y {number|string|Decimal} The exponent.
*
*/
function pow(x, y) {
return new this(x).pow(y);
}
/*
* Returns a new Decimal with a random value equal to or greater than 0 and less than 1, and with
* `sd`, or `Decimal.precision` if `sd` is omitted, significant digits (or less if trailing zeros
* are produced).
*
* [sd] {number} Significant digits. Integer, 0 to MAX_DIGITS inclusive.
*
*/
function random(sd) {
var d, e, k, n,
i = 0,
r = new this(1),
rd = [];
if (sd === void 0) sd = this.precision;
else checkInt32(sd, 1, MAX_DIGITS);
k = Math.ceil(sd / LOG_BASE);
if (!this.crypto) {
for (; i < k;) rd[i++] = Math.random() * 1e7 | 0;
// Browsers supporting crypto.getRandomValues.
} else if (crypto.getRandomValues) {
d = crypto.getRandomValues(new Uint32Array(k));
for (; i < k;) {
n = d[i];
// 0 <= n < 4294967296
// Probability n >= 4.29e9, is 4967296 / 4294967296 = 0.00116 (1 in 865).
if (n >= 4.29e9) {
d[i] = crypto.getRandomValues(new Uint32Array(1))[0];
} else {
// 0 <= n <= 4289999999
// 0 <= (n % 1e7) <= 9999999
rd[i++] = n % 1e7;
}
}
// Node.js supporting crypto.randomBytes.
} else if (crypto.randomBytes) {
// buffer
d = crypto.randomBytes(k *= 4);
for (; i < k;) {
// 0 <= n < 2147483648
n = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + ((d[i + 3] & 0x7f) << 24);
// Probability n >= 2.14e9, is 7483648 / 2147483648 = 0.0035 (1 in 286).
if (n >= 2.14e9) {
crypto.randomBytes(4).copy(d, i);
} else {
// 0 <= n <= 2139999999
// 0 <= (n % 1e7) <= 9999999
rd.push(n % 1e7);
i += 4;
}
}
i = k / 4;
} else {
throw Error(cryptoUnavailable);
}
k = rd[--i];
sd %= LOG_BASE;
// Convert trailing digits to zeros according to sd.
if (k && sd) {
n = mathpow(10, LOG_BASE - sd);
rd[i] = (k / n | 0) * n;
}
// Remove trailing words which are zero.
for (; rd[i] === 0; i--) rd.pop();
// Zero?
if (i < 0) {
e = 0;
rd = [0];
} else {
e = -1;
// Remove leading words which are zero and adjust exponent accordingly.
for (; rd[0] === 0; e -= LOG_BASE) rd.shift();
// Count the digits of the first word of rd to determine leading zeros.
for (k = 1, n = rd[0]; n >= 10; n /= 10) k++;
// Adjust the exponent for leading zeros of the first word of rd.
if (k < LOG_BASE) e -= LOG_BASE - k;
}
r.e = e;
r.d = rd;
return r;
}
/*
* Return a new Decimal whose value is `x` rounded to an integer using rounding mode `rounding`.
*
* To emulate `Math.round`, set rounding to 7 (ROUND_HALF_CEIL).
*
* x {number|string|Decimal}
*
*/
function round(x) {
return finalise(x = new this(x), x.e + 1, this.rounding);
}
/*
* Return
* 1 if x > 0,
* -1 if x < 0,
* 0 if x is 0,
* -0 if x is -0,
* NaN otherwise
*
* x {number|string|Decimal}
*
*/
function sign(x) {
x = new this(x);
return x.d ? (x.d[0] ? x.s : 0 * x.s) : x.s || NaN;
}
/*
* Return a new Decimal whose value is the sine of `x`, rounded to `precision` significant digits
* using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function sin(x) {
return new this(x).sin();
}
/*
* Return a new Decimal whose value is the hyperbolic sine of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function sinh(x) {
return new this(x).sinh();
}
/*
* Return a new Decimal whose value is the square root of `x`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal}
*
*/
function sqrt(x) {
return new this(x).sqrt();
}
/*
* Return a new Decimal whose value is `x` minus `y`, rounded to `precision` significant digits
* using rounding mode `rounding`.
*
* x {number|string|Decimal}
* y {number|string|Decimal}
*
*/
function sub(x, y) {
return new this(x).sub(y);
}
/*
* Return a new Decimal whose value is the sum of the arguments, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* Only the result is rounded, not the intermediate calculations.
*
* arguments {number|string|Decimal}
*
*/
function sum() {
var i = 0,
args = arguments,
x = new this(args[i]);
external = false;
for (; x.s && ++i < args.length;) x = x.plus(args[i]);
external = true;
return finalise(x, this.precision, this.rounding);
}
/*
* Return a new Decimal whose value is the tangent of `x`, rounded to `precision` significant
* digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function tan(x) {
return new this(x).tan();
}
/*
* Return a new Decimal whose value is the hyperbolic tangent of `x`, rounded to `precision`
* significant digits using rounding mode `rounding`.
*
* x {number|string|Decimal} A value in radians.
*
*/
function tanh(x) {
return new this(x).tanh();
}
/*
* Return a new Decimal whose value is `x` truncated to an integer.
*
* x {number|string|Decimal}
*
*/
function trunc(x) {
return finalise(x = new this(x), x.e + 1, 1);
}
// Create and configure initial Decimal constructor.
Decimal = clone(DEFAULTS);
Decimal.prototype.constructor = Decimal;
Decimal['default'] = Decimal.Decimal = Decimal;
// Create the internal constants from their string values.
LN10 = new Decimal(LN10);
PI = new Decimal(PI);
// Export.
// AMD.
if (typeof define == 'function' && define.amd) {
define(function () {
return Decimal;
});
// Node and other environments that support module.exports.
} else if (typeof module != 'undefined' && module.exports) {
if (typeof Symbol == 'function' && typeof Symbol.iterator == 'symbol') {
P[Symbol['for']('nodejs.util.inspect.custom')] = P.toString;
P[Symbol.toStringTag] = 'Decimal';
}
module.exports = Decimal;
// Browser.
} else {
if (!globalScope) {
globalScope = typeof self != 'undefined' && self && self.self == self ? self : window;
}
noConflict = globalScope.Decimal;
Decimal.noConflict = function () {
globalScope.Decimal = noConflict;
return Decimal;
};
globalScope.Decimal = Decimal;
}
})(this);
================================================
FILE: src/lib/eventemitter3.js
================================================
// source: https://github.com/primus/eventemitter3/blob/master/index.js
'use strict';
var has = Object.prototype.hasOwnProperty
, prefix = '~';
/**
* Constructor to create a storage for our `EE` objects.
* An `Events` instance is a plain object whose properties are event names.
*
* @constructor
* @private
*/
function Events() {}
//
// We try to not inherit from `Object.prototype`. In some engines creating an
// instance in this way is faster than calling `Object.create(null)` directly.
// If `Object.create(null)` is not supported we prefix the event names with a
// character to make sure that the built-in object properties are not
// overridden or used as an attack vector.
//
if (Object.create) {
Events.prototype = Object.create(null);
//
// This hack is needed because the `__proto__` property is still inherited in
// some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
//
if (!new Events().__proto__) prefix = false;
}
/**
* Representation of a single event listener.
*
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} [once=false] Specify if the listener is a one-time listener.
* @constructor
* @private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Add a listener for a given event.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} once Specify if the listener is a one-time listener.
* @returns {EventEmitter}
* @private
*/
function addListener(emitter, event, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('The listener must be a function');
}
var listener = new EE(fn, context || emitter, once)
, evt = prefix ? prefix + event : event;
if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
else emitter._events[evt] = [emitter._events[evt], listener];
return emitter;
}
/**
* Clear event by name.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} evt The Event name.
* @private
*/
function clearEvent(emitter, evt) {
if (--emitter._eventsCount === 0) emitter._events = new Events();
else delete emitter._events[evt];
}
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*
* @constructor
* @public
*/
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*
* @returns {Array}
* @public
*/
EventEmitter.prototype.eventNames = function eventNames() {
var names = []
, events
, name;
if (this._eventsCount === 0) return names;
for (name in (events = this._events)) {
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
/**
* Return the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Array} The registered listeners.
* @public
*/
EventEmitter.prototype.listeners = function listeners(event) {
var evt = prefix ? prefix + event : event
, handlers = this._events[evt];
if (!handlers) return [];
if (handlers.fn) return [handlers.fn];
for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
ee[i] = handlers[i].fn;
}
return ee;
};
/**
* Return the number of listeners listening to a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Number} The number of listeners.
* @public
*/
EventEmitter.prototype.listenerCount = function listenerCount(event) {
var evt = prefix ? prefix + event : event
, listeners = this._events[evt];
if (!listeners) return 0;
if (listeners.fn) return 1;
return listeners.length;
};
/**
* Calls each of the listeners registered for a given event.
*
* @param {(String|Symbol)} event The event name.
* @returns {Boolean} `true` if the event had listeners, else `false`.
* @public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
if (listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Add a listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
return addListener(this, event, fn, context, false);
};
/**
* Add a one-time listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
return addListener(this, event, fn, context, true);
};
/**
* Remove the listeners of a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn Only remove the listeners that match this function.
* @param {*} context Only remove the listeners that have this context.
* @param {Boolean} once Only remove one-time listeners.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return this;
if (!fn) {
clearEvent(this, evt);
return this;
}
var listeners = this._events[evt];
if (listeners.fn) {
if (
listeners.fn === fn &&
(!once || listeners.once) &&
(!context || listeners.context === context)
) {
clearEvent(this, evt);
}
} else {
for (var i = 0, events = [], length = listeners.length; i < length; i++) {
if (
listeners[i].fn !== fn ||
(once && !listeners[i].once) ||
(context && listeners[i].context !== context)
) {
events.push(listeners[i]);
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
else clearEvent(this, evt);
}
return this;
};
/**
* Remove all listeners, or those of the specified event.
*
* @param {(String|Symbol)} [event] The event name.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
var evt;
if (event) {
evt = prefix ? prefix + event : event;
if (this._events[evt]) clearEvent(this, evt);
} else {
this._events = new Events();
this._eventsCount = 0;
}
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;
//
// Expose the module.
//
if ('undefined' !== typeof module) {
module.exports = EventEmitter;
}
================================================
FILE: src/lib/jquery-ui-1.12.1/AUTHORS.txt
================================================
Authors ordered by first contribution
A list of current team members is available at http://jqueryui.com/about
Paul Bakaus
Richard Worth
Yehuda Katz
Sean Catchpole
John Resig
Tane Piper
Dmitri Gaskin
Klaus Hartl
Stefan Petre
Gilles van den Hoven
Micheil Bryan Smith
Jörn Zaefferer
Marc Grabanski
Keith Wood
Brandon Aaron
Scott González
Eduardo Lundgren
Aaron Eisenberger
Joan Piedra
Bruno Basto
Remy Sharp
Bohdan Ganicky
David Bolter
Chi Cheng
Ca-Phun Ung
Ariel Flesler
Maggie Wachs
Scott Jehl
Todd Parker
Andrew Powell
Brant Burnett
Douglas Neiner
Paul Irish
Ralph Whitbeck
Thibault Duplessis
Dominique Vincent
Jack Hsu
Adam Sontag
Carl Fürstenberg
Kevin Dalman
Alberto Fernández Capel
Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
Ting Kuei
Samuel Cormier-Iijima
Jon Palmer
Ben Hollis
Justin MacCarthy
Eyal Kobrigo
Tiago Freire
Diego Tres
Holger Rüprich
Ziling Zhao
Mike Alsup
Robson Braga Araujo
Pierre-Henri Ausseil
Christopher McCulloh
Andrew Newcomb
Lim Chee Aun
Jorge Barreiro
Daniel Steigerwald
John Firebaugh
John Enters
Andrey Kapitcyn
Dmitry Petrov
Eric Hynds
Chairat Sunthornwiphat
Josh Varner
Stéphane Raimbault
Jay Merrifield
J. Ryan Stinnett
Peter Heiberg
Alex Dovenmuehle
Jamie Gegerson
Raymond Schwartz
Phillip Barnes
Kyle Wilkinson
Khaled AlHourani
Marian Rudzynski
Jean-Francois Remy
Doug Blood
Filippo Cavallarin
Heiko Henning
Aliaksandr Rahalevich
Mario Visic
Xavi Ramirez
Max Schnur
Saji Nediyanchath
Corey Frang
Aaron Peterson
Ivan Peters
Mohamed Cherif Bouchelaghem
Marcos Sousa
Michael DellaNoce
George Marshall
Tobias Brunner
Martin Solli
David Petersen
Dan Heberden
William Kevin Manire
Gilmore Davidson
Michael Wu
Adam Parod
Guillaume Gautreau
Marcel Toele
Dan Streetman
Matt Hoskins
Giovanni Giacobbi
Kyle Florence
Pavol Hluchý
Hans Hillen
Mark Johnson
Trey Hunner
Shane Whittet
Edward A Faulkner
Adam Baratz
Kato Kazuyoshi
Eike Send
Kris Borchers
Eddie Monge
Israel Tsadok
Carson McDonald
Jason Davies
Garrison Locke
David Murdoch
Benjamin Scott Boyle
Jesse Baird
Jonathan Vingiano
Dylan Just
Hiroshi Tomita
Glenn Goodrich
Tarafder Ashek-E-Elahi
Ryan Neufeld
Marc Neuwirth
Philip Graham
Benjamin Sterling
Wesley Walser
Kouhei Sutou
Karl Kirch
Chris Kelly
Jason Oster
Felix Nagel
Alexander Polomoshnov
David Leal
Igor Milla
Dave Methvin
Florian Gutmann
Marwan Al Jubeh
Milan Broum
Sebastian Sauer
Gaëtan Muller
Michel Weimerskirch
William Griffiths
Stojce Slavkovski
David Soms
David De Sloovere
Michael P. Jung
Shannon Pekary
Dan Wellman
Matthew Edward Hutton
James Khoury
Rob Loach
Alberto Monteiro
Alex Rhea
Krzysztof Rosiński
Ryan Olton
Genie <386@mail.com>
Rick Waldron
Ian Simpson
Lev Kitsis
TJ VanToll
Justin Domnitz
Douglas Cerna
Bert ter Heide
Jasvir Nagra
Yuriy Khabarov <13real008@gmail.com>
Harri Kilpiö
Lado Lomidze
Amir E. Aharoni
Simon Sattes
Jo Liss
Guntupalli Karunakar
Shahyar Ghobadpour
Lukasz Lipinski
Timo Tijhof
Jason Moon
Martin Frost
Eneko Illarramendi
EungJun Yi
Courtland Allen
Viktar Varvanovich
Danny Trunk
Pavel Stetina
Michael Stay
Steven Roussey
Michael Hollis
Lee Rowlands
Timmy Willison
Karl Swedberg
Baoju Yuan
Maciej Mroziński
Luis Dalmolin
Mark Aaron Shirley
Martin Hoch
Jiayi Yang
Philipp Benjamin Köppchen
Sindre Sorhus
Bernhard Sirlinger
Jared A. Scheel
Rafael Xavier de Souza
John Chen
Robert Beuligmann
Dale Kocian
Mike Sherov
Andrew Couch
Marc-Andre Lafortune
Nate Eagle
David Souther
Mathias Stenbom
Sergey Kartashov
Avinash R
Ethan Romba
Cory Gackenheimer
Juan Pablo Kaniefsky
Roman Salnikov
Anika Henke
Samuel Bovée
Fabrício Matté
Viktor Kojouharov
Pawel Maruszczyk (http://hrabstwo.net)
Pavel Selitskas
Bjørn Johansen
Matthieu Penant
Dominic Barnes
David Sullivan
Thomas Jaggi
Vahid Sohrabloo
Travis Carden
Bruno M. Custódio
Nathanael Silverman
Christian Wenz
Steve Urmston
Zaven Muradyan
Woody Gilk
Zbigniew Motyka
Suhail Alkowaileet
Toshi MARUYAMA
David Hansen
Brian Grinstead
Christian Klammer
Steven Luscher
Gan Eng Chin
Gabriel Schulhof
Alexander Schmitz
Vilhjálmur Skúlason
Siebrand Mazeland
Mohsen Ekhtiari
Pere Orga