Full Code of SeanArchibald/scale-workshop for AI

master f84432ca94e3 cached
62 files
2.2 MB
575.2k tokens
404 symbols
1 requests
Download .txt
Showing preview only (2,300K chars total). Download the full file or copy to clipboard to get everything.
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

![Scale Workshop screenshot](https://raw.githubusercontent.com/SeanArchibald/scale-workshop/master/src/assets/img/scale-workshop-og-image.png)


## 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
================================================
<!--
  ____            _
 / ___|  ___ __ _| | ___
 \___ \ / __/ _` | |/ _ \
  ___) | (_| (_| | |  __/
 |____/ \___\__,_|_|\___|
 __        __         _        _
 \ \      / /__  _ __| | _____| |__   ___  _ __
  \ \ /\ / / _ \| '__| |/ / __| '_ \ / _ \| '_ \
   \ V  V / (_) | |  |   <\__ \ | | | (_) | |_) |
    \_/\_/ \___/|_|  |_|\_\___/_| |_|\___/| .__/
                                          |_|
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Scale Workshop User Guide</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Open Graph -->
  <meta property="og:title" content="Scale Workshop User Guide" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://sevish.com/scaleworkshop/guide.htm" />
  <meta property="og:image" content="https://sevish.com/scaleworkshop/src/assets/img/scale-workshop-og-image.png" />
  <meta property="og:description"
    content="Scale Workshop is a tool that allows you to create microtonal tunings within your web browser. You can export these tunings to your device and use them to tune various synthesizers." />
  <meta property="og:site_name" content="Sevish Music" />

  <!-- Styles -->
  <link rel="stylesheet" href="src/lib/bootstrap-3.3.7-dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="src/lib/jquery-ui-1.12.1/jquery-ui.min.css">
  <link rel="stylesheet" href="src/lib/socicon/style.css">

  <!-- Scripts -->
  <script src="src/lib/jquery-3.2.1.min.js"></script>
  <script src="src/lib/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>

  <!-- Favicon -->
  <link rel="apple-touch-icon" sizes="180x180" href="src/assets/favicon/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="src/assets/favicon/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="src/assets/favicon/favicon-16x16.png">
  <link rel="manifest" href="src/assets/favicon/manifest.json">
  <link rel="mask-icon" href="src/assets/favicon/safari-pinned-tab.svg" color="#9bd03b">
  <meta name="theme-color" content="#ffffff">

  <style>
    body {
      max-width: 1160px;
      position: relative;
    }

    img {
      max-width: 100%;
    }

    header,
    article {
      padding: 2em 15px;
    }

    nav {
      margin-bottom: 2em;
      padding: 0px 15px;
    }

    nav a:link {
      color: unset;
    }

    nav ul {
      padding-left: 0px;
      list-style-type: none;
      font-weight: bold;
    }

    nav ul li ul {
      padding-left: 1em;
    }

    nav ul li ul li {
      font-weight: normal;
    }

    nav .btn {
      margin-top: 0.5em;
      width: 100%;
    }

    h1 {
      margin-top: 0px;
      padding-top: 0.8em;
    }

    h2 {
      margin-top: 2em;
      padding-top: 1em;
    }

    h3 {
      margin-top: 1em;
      padding-top: 1em;
    }

    h4 {
      margin-top: 1em;
      padding-top: 1em;
    }

    header h1 {
      padding-top: 0.6em;
    }

    header h2,
    header h3,
    header h4 {
      padding: 0;
      margin-top: 0;
    }

    p.lead {
      clear: both;
      font-weight: normal;
    }

    nav a:visited {
      color: black;
    }

    #logo {
      width: 9.5em;
      max-width: 100%;
      height: auto;
      text-align: center;
      display: block;
      margin: auto;
    }

    /* Non-smartphone devices (tablet size and up) */
    @media (min-width: 768px) {

      header,
      article {
        width: calc(100% - 300px);
        margin-left: 300px;
      }

      nav {
        position: fixed !important;
        width: 300px;
        height: 100vh;
        overflow-y: scroll;
        top: 0px;
        left: 0px;
        padding: 2em 15px;
      }

      img {
        max-width: 340px;
        /*margin-left: auto;
          margin-right: auto;
          display: block;*/
      }

      p.lead {
        font-weight: 300;
      }

      #logo {
        float: left;
        margin: auto 1.5em 1.5em 0px;
      }
    }
  </style>

</head>

<body>
  <header>
    <p><img src="src/assets/favicon/apple-touch-icon.png" alt="Scale Workshop logo" id="logo" /></p>
    <h1>Scale Workshop</h1>
    <h4>User guide 1.5</h4>
    <p class="lead">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.</p>
    <a href="#overview" class="sr-only">Skip to main content</a>
  </header>
  <nav id="main-nav">
    <ul>
      <li style="font-size:1.2em;"><a href="#">Scale Workshop User Guide</a></li>
      <li><a href="#overview">Overview</a></li>
      <li><a href="#import-export-tunings">Import/export tunings</a>
        <ul>
          <li><a href="#import">Import</a></li>
          <li><a href="#convert">Convert</a></li>
          <li><a href="#export">Export</a>
            <ul>
              <li><a href="#anamark-tun">Anamark TUN (.tun)</a></li>
              <li><a href="#scala-scl">Scala scale (.scl)</a></li>
              <li><a href="#scala-kbm">Scala keyboard mapping (.kbm)</a></li>
              <li><a href="#maxmsp">Max/MSP coll (.txt)</a></li>
              <li><a href="#puredata">PureData text (.txt)</a></li>
              <li><a href="#kontakt">Kontakt script (.txt)</a></li>
              <li><a href="#soniccouture">Soniccouture tuning file (.nka)</a></li>
              <li><a href="#harmor">Harmor Pitch Map (.fnv)</a></li>
              <li><a href="#sytrus">Sytrus Pitch Map (.fnv)</a></li>
              <li><a href="#korg-logue">Korg Sound Librarian Scale/Octave (.mnlgtuns/mnlgtuno)</a></li>
              <li><a href="#deflemask">Deflemask 'fine tune' reference</a></li>
              <li><a href="#reaper-note-names">Reaper Note Name Map (.txt)</a></li>
            </ul>
          </li>
          <li><a href="#presets">Presets</a></li>
        </ul>
      </li>
      <li><a href="#scale-design">Scale design</a>
        <ul>
          <li><a href="#equal-temperaments">Equal temperaments</a></li>
          <li><a href="#rank-2-temperaments">Rank-2 temperaments</a></li>
          <li><a href="#harmonic-series-segments">Harmonic series segments</a></li>
          <li><a href="#subharmonic-series-segments">Subharmonic series segments</a></li>
          <li><a href="#enumerate-chord">Enumerate chord</a></li>
          <li><a href="#cps">Combination product set</a></li>
          <li><a href="#manual-data-entry">Manual data entry</a>
            <ul>
              <li><a href="#just-intonation">Just intonation</a></li>
              <li><a href="#equal-temperament">Equal temperament</a></li>
              <li><a href="#misc">Misc</a></li>
            </ul>
          </li>
          <li><a href="#modify-scales">Modify scales</a>
            <ul>
              <li><a href="#sort-ascending">Sort ascending</a></li>
              <li><a href="#reduce">Reduce</a></li>
              <li><a href="#rotate">Rotate</a></li>
              <li><a href="#subset">Subset</a></li>
              <li><a href="#stretch-compress">Stretch/compress</a></li>
              <li><a href="#random-variance">Random variance</a></li>
              <li><a href="#tempo-sync-beating">Tempo-sync beating</a></li>
              <li><a href="#approximate-by-ratios">Approximate by ratios</a></li>
              <li><a href="#approximate-by-harmonics">Approximate by harmonics</a></li>
              <li><a href="#approximate-by-subharmonics">Approximate by subharmonics</a></li>
              <li><a href="#equalize">Equalize</a></li>
            </ul>
          </li>
        </ul>
      </li>
      <li><a href="#synthesizer">Synthesizer</a>
        <ul>
          <li><a href="#playing-with-midi">Playing with a MIDI controller</a></li>
          <li><a href="#playing-with-qwerty">Playing with QWERTY</a></li>
          <li><a href="#playing-with-touch-screen">Playing with mouse / touch screen</a></li>
          <li><a href="#isomorphic-mapping">Isomorphic mapping</a></li>
          <li><a href="#changing-the-sound-of-the-synth">Changing the sound of the synth</a></li>
        </ul>
      </li>
      <li><a href="#midi">MIDI I/O</a></li>
      <li><a href="#misc-tips">Misc. tips</a></li>
    </ul>
    <a class="btn btn-default" href="index.htm" target="_blank">Launch Scale Workshop</a>
  </nav>

  <article>
    <h2 id="overview">Overview</h2>
    <p class="text-center"><a href="src/assets/img/scale-workshop-og-image.png" target="_blank"><img
          src="src/assets/img/scale-workshop-og-image.png" style="width:100%; max-width:100%;" /></a></p>
    <p>The interface is split in to 4 sections.</p>
    <ul>
      <li>Top bar: access the main functions of the software</li>
      <li>Left column: manual entry of scale data</li>
      <li>Center column: table of tuning data</li>
      <li>Right column: options and configuration</li>
    </ul>

    <p>The top bar provides many useful functions.</p>
    <ul>
      <li>New: generate a new scale or load one from a file</li>
      <li>Modify: apply some transformation to a scale that is currently loaded</li>
      <li>Export: save your work in one of many formats</li>
      <li>About: about Scale Workshop</li>
    </ul>


    <h2 id="import-export-tunings">Import/export tunings</h2>
    <h3 id="import">Import</h3>
    <p>Scale Workshop supports import of <strong>Scala .scl</strong> files, <strong>AnaMark .tun</strong> files and Korg 'logue .mnlgtuns/.mnlgtuno'.
      Click 'New' on the top menu bar and then click 'Import .scl' or 'Import .tun'</p>
    <p class="text-center"><a href="src/assets/img/guide/menu-new.png" target="_blank"><img
          src="src/assets/img/guide/menu-new.png" /></a></p>
    <p><strong>Note:</strong> AnaMark .tun import is incomplete, but it should be able to import .tun V2.00 files exported by
      Scale Workshop and Scala.</p>

    <h3 id="convert">Convert</h3>
    <p>Convert .scl or convert .tun tunings by importing them and then exporting.</p>

    <h3 id="export">Export</h3>
    <p>Many synthesizers support microtonal scales. Usually they will require some format of tuning file. Scale Workshop
      supports many of the popular formats.</p>
    <p>To see a list of synths that support microtonal scales, see the <a
        href="https://en.xen.wiki/w/List_of_Microtonal_Software_Plugins" target="_blank">List of Microtonal Software
        Plugins</a> on the Xenharmonic Wiki.</p>
    <p class="text-center"><a href="src/assets/img/guide/menu-export.png" target="_blank"><img
          src="src/assets/img/guide/menu-export.png" /></a></p>

    <h4 id="anamark-tun">AnaMark TUN (.tun)</h4>
    <p>If your synth supports .tun format, then please refer to its manual to find how to load the .tun file.</p>

    <h4 id="scala-scl">Scala scale (.scl)</h4>
    <p>If your synth supports .scl format, then please refer to its manual to find how to load the .scl file.</p>

    <h4 id="scala-kbm">Scala keyboard mapping (.kbm)</h4>
    <p>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.</p>

    <h4 id="maxmsp">Max/MSP coll (.txt)</h4>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/maxmsp-coll.png" target="_blank"><img
          src="src/assets/img/guide/maxmsp-coll.png" /></a></p>
    <p>Then when you input MIDI note numbers to the coll object, it will output the desired frequency.</p>

    <h4 id="puredata">PureData text (.txt)</h4>
    <p>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.</p>

    <h4 id="kontakt">Kontakt script (.txt)</h4>
    <p>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.</p>

    <h4 id="soniccouture">Soniccouture tuning file (.nka)</h4>
    <p>Tuning format for Soniccouture's sampled instruments for Kontakt.</p>

    <h4 id="harmor">Harmor Pitch Map (.fnv)</h4>
    <p>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).</p>

    <h4 id="sytrus">Sytrus Pitch Map (.fnv)</h4>
    <p>Sytrus allows retuning keys from C0 to C10 for up to 4 octaves up or down from 12 EDO.
      <b>Warning: You won't be able to slide notes. If you do, the pitch will be incorrect.</b>
      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.</p>

    <h4 id="korg-logue">Korg Sound Librarian Scale/Octave (.mnlgtuns/.mnlgtuno)</h4>
    <p>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.</p>
    
    <h4 id="deflemask">Deflemask 'fine tune' reference (.txt)</h4>
    <p>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.</p>

    <h4 id="reaper-note-names">Reaper Note Name Map (.txt)</h4>
    <p>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.</br>
      <b>Show Period Number</b> will put the octave/period number of the interval after the pitch label.</br>
      <b>Calculate Period in Pitch</b> 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.</br>
      <b>Base Period Number</b> 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.</br>
      <b>Base Cents Value</b> and <b>Base Degree Number</b> 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.</p>
    <p>Reaper will look for these files by default in:<ul>
        <li>Windows - C:\Users\[username]\AppData\Roaming\REAPER\MIDINoteNames</li>
        <li>macOS - /Users/[username]/Library/Application Support/REAPER/MIDINoteNames</li></ul>
      A shortcut to the REAPER folder is in Reaper's Option menu, "Show REAPER resource folder in explorer/finder".</br>
      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".</br>
      <i>Note: These are only labels - you must retune the synth to hear the displayed notes in the piano roll.</i></p>
    <p class="text-center"><a href="src/assets/img/guide/reaper-named-notes.png" target="_blank"><img
          src="src/assets/img/guide/reaper-named-notes.png"></a></p>

    <h3 id="presets">Presets</h3>
    <p>A selection of preset scales are provided as examples.</p>
    <p class="text-center"><a href="src/assets/img/guide/preset-scales.png" target="_blank"><img
          src="src/assets/img/guide/preset-scales.png" /></a></p>


    <h2 id="scale-design">Scale design</h2>
    <p>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.</p>
    <p>Start by clicking the <strong>New</strong> option on the top menu bar. Select from equal temperament, rank-2
      temperament, etc.</p>
    <p class="text-center"><a href="src/assets/img/guide/menu-new.png" target="_blank"><img
          src="src/assets/img/guide/menu-new.png" /></a></p>

    <h3 id="equal-temperaments">Equal temperaments</h3>
    <p><a href="https://en.xen.wiki/w/Equal-step_tuning" target="_blank">Equal temperaments</a> 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.</p>
    <p>To create a new equal temperament scale click <strong>New &gt; Equal temperament</strong> and then enter values
      to create the scale.</p>
    <p class="text-center"><a href="src/assets/img/guide/equal-temperament.png" target="_blank"><img
          src="src/assets/img/guide/equal-temperament.png" /></a></p>
    <p>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.</p>

    <h3 id="rank-2-temperaments">Rank-2 temperaments</h3>
    <p>Create a rank-2 temperament using a generator and period.</p>
    <p class="text-center"><a href="src/assets/img/guide/rank-2-temperament.png" target="_blank"><img
          src="src/assets/img/guide/rank-2-temperament.png" /></a></p>

    <h3 id="harmonic-series-segments">Harmonic series segments</h3>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/harmonic-series.png" target="_blank"><img
          src="src/assets/img/guide/harmonic-series.png" /></a></p>

    <h3 id="subharmonic-series-segments">Subharmonic series segments</h3>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/subharmonic-series.png" target="_blank"><img
          src="src/assets/img/guide/subharmonic-series.png" /></a></p>

    <h3 id="enumerate-chord">Enumerate chord</h3>
    <p>Create a scale from a list of harmonics separated by colons.<br/>E.g. 4:5:6:7:8</p>
    <p class="text-center"><a href="src/assets/img/guide/enumerate-chord.png" target="_blank"><img src="src/assets/img/guide/enumerate-chord.png" /></a></p>

    <h3 id="cps">Combination product set</h3>
    <p>Create a scale by multiplying harmonics in different combinations. Optionally, the resulting scale can be reduced by 2/1 and sorted in ascending order.</p>
    <p><a href="https://en.xen.wiki/w/Combination_product_set" target="_blank">Combination product set (Xenharmonic Wiki)</a></p>
    <p class="text-center"><a href="src/assets/img/guide/cps.png" target="_blank"><img src="src/assets/img/guide/cps.png" /></a></p>

    <h3 id="manual-data-entry">Manual data entry</h3>
    <p>Scales can be written manually by typing them in to the <strong>Scale data</strong> 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.</p>
    <p class="text-center"><a href="src/assets/img/guide/scale-entry.png" target="_blank"><img
          src="src/assets/img/guide/scale-entry.png" /></a></p>
    <p>Values with a . are cents values: e.g. 701.955<br />
      Values with slash (/) are ratios: e.g. 3/2<br />
      Values with a backslash (n\m) are n degrees of m-EDO: e.g. 7\12<br />
      Values with comma (,) are decimals: e.g. 1,5<br />
      The final value is your octave or pseudo-octave: e.g. 2/1</p>

    <h4 id="just-intonation">Just intonation examples</h4>
    <p>5-limit just major:</p>
    <pre>9/8
5/4
4/3
3/2
5/3
15/8
2/1</pre>
    <p>Harmonics 8-16:</p>
    <pre>9/8
10/8
11/8
12/8
13/8
14/8
15/8
16/8</pre>

    <h4 id="equal-temperament">Equal temperament examples</h4>
    <p>Blackwood[10] in 15edo:</p>
    <pre>2\15
3\15
5\15
6\15
8\15
9\15
11\15
12\15
14\15
15\15</pre>
    <p>8edo in cents:</p>
    <pre>150.
300.
450.
600.
750.
900.
1050.
1200.</pre>

    <h4 id="misc">Misc examples</h4>
    <p>You can combine any of the above styles in the same scale if needed:</p>
    <pre>1,2
3\9
700.1
2/1</pre>


    <h3 id="modify-scales">Modify scales</h3>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/menu-modify.png" target="_blank"><img
          src="src/assets/img/guide/menu-modify.png" /></a></p>

    <h3 id="sort-ascending">Sort ascending</h3>
    <p>Sorts your scale ascendingly.</p>
    
    <h3 id="reduce">Reduce</h3>
    <p>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.</p>

    <h3 id="rotate">Rotate</h3>
    <p>Allows you to choose an interval of your scale to become 1/1.</p>

    <h3 id="subset">Subset</h3>
    <p>Takes a subset of the current scale. Subsets are entered numerically.</p>
    <p class="text-center"><a href="src/assets/img/guide/subset.png" target="_blank"><img
          src="src/assets/img/guide/subset.png" /></a></p>

    <h3 id="stretch-compress">Stretch/compress</h3>
    <p>Applies a linear stretch across the current scale. Enter a stretch factor, where 1 is no stretch at all.</p>
    <p class="text-center"><a href="src/assets/img/guide/stretch-compress.png" target="_blank"><img
          src="src/assets/img/guide/stretch-compress.png" /></a></p>

    <h3 id="random-variance">Random variance</h3>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/random-variance.png" target="_blank"><img
          src="src/assets/img/guide/random-variance.png" /></a></p>

    <h3 id="tempo-sync-beating">Tempo-sync beating</h3>
    <p>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.</p>
    <p>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).</p>
    <p class="text-center"><a href="src/assets/img/guide/tempo-sync-beating.png" target="_blank"><img
          src="src/assets/img/guide/tempo-sync-beating.png" /></a></p>
    <p>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.
    </p>

    <h3 id="approximate-by-ratios">Approximate by ratios</h3>
    <p>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.</p>

    <h3 id="approximate-by-harmonics">Approximate by harmonics</h3>
    <p>This retunes each interval in your scale to the nearest harmonic. You select the denominator.</p>

    <h3 id="approximate-by-subharmonics">Approximate by subharmonics</h3>
    <p>This retunes each interval in your scale to the nearest subharmonic. You select the numerator.</p>

    <h3 id="equalize">Equalize</h3>
    <p>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.</p>    

    <h2 id="synthesizer">Synthesizer</h2>
    <p>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.</p>

    <h3 id="playing-with-midi">Playing with a MIDI controller</h3>
    <p>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.</p>

    <h3 id="playing-with-qwerty">Playing with QWERTY</h3>
    <p>Use your computer keyboard to play your current scale, much like an isomorphic keyboard.</p>
    <p><strong>Note:</strong> 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:</p>
    <p class="text-center"><a href="src/assets/img/guide/qwerty-enabled.png" target="_blank"><img
          src="src/assets/img/guide/qwerty-enabled.png" /></a></p>
    <p class="text-center"><a href="src/assets/img/guide/qwerty-disabled.png" target="_blank"><img
          src="src/assets/img/guide/qwerty-disabled.png" /></a></p>
    <p>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.</p>

    <h3 id="playing-with-touch-screen">Playing with mouse / touch screen</h3>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/virtual-keyboard.png" target="_blank"><img
          src="src/assets/img/guide/virtual-keyboard.png" /></a></p>

    <h3 id="isomorphic-mapping">Isomorphic mapping</h3>
    <p>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
      <strong>Isomorphic keyboard settings</strong> shown on the right column of the Scale Workshop interface.</p>
    <p class="text-center"><a href="src/assets/img/guide/isomorphic-settings.png" target="_blank"><img
          src="src/assets/img/guide/isomorphic-settings.png" /></a></p>

    <h3 id="changing-the-sound-of-the-synth">Changing the sound of the synth</h3>
    <p>The synth has the following options:</p>
    <ul>
      <li><strong>Main Volume</strong></li>
      <li><strong>Waveform</strong>: Semisine (default), Octaver, Triangle, Square, Sawtooth, Brightness, Harmonic Bell, Warm, Sine</li>
      <li><strong>Amplitude envelope</strong>: Organ, Pad, Percussive (Short, Medium and Long)</li>
      <li><strong>Delay effect</strong>: feedback delay echo</li>
      <li><strong>Max polyphony</strong>: 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.</li>
    </ul>
    <p class="text-center"><a href="src/assets/img/guide/synth-settings.png" target="_blank"><img
          src="src/assets/img/guide/synth-settings.png" /></a></p>
    
    <h2 id="midi">MIDI I/O</h2>
    <p>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.</p>
    <p>Before getting started, make sure that your web browser is web-MIDI compatible. Not all browsers support MIDI I/O.</p>
    <p>On your MIDI synth device, you should set pitch bend range to +/- 1 octave.</p>
    <p>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.</p>
    <p class="text-center"><a href="src/assets/img/guide/midi-io-settings.png" target="_blank"><img src="src/assets/img/guide/midi-io-settings.png" /></a></p>
    <p>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.</p>

    <h2 id="misc-tips">Misc. tips</h2>
    <p><strong>General settings</strong> can be found on the right side of the Scale Workshop interface.</p>
    <p class="text-center"><a href="src/assets/img/guide/general-settings.png" target="_blank"><img
          src="src/assets/img/guide/general-settings.png" /></a></p>
    <p>If an exported tuning file doesn't seem to load into a softsynth properly, then try changing the value of
      <strong>Line endings format</strong> 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.</p>
    <p><strong>Dark mode</strong> makes the Scale Workshop interface dark.</p>
    <p class="text-center"><a href="src/assets/img/guide/dark-mode.png" target="_blank"><img
          src="src/assets/img/guide/dark-mode.png" /></a></p>
    <p>Undo/redo your tuning changes by using the back/forward browser navigation buttons.</p>
    <p>If the synth gets too noisy, you can kill all sound by clicking the <strong>Quiet</strong> button.</p>
  </article>
</body>
</html>

================================================
FILE: index.htm
================================================
<!--
  ____            _
 / ___|  ___ __ _| | ___
 \___ \ / __/ _` | |/ _ \
  ___) | (_| (_| | |  __/
 |____/ \___\__,_|_|\___|
 __        __         _        _
 \ \      / /__  _ __| | _____| |__   ___  _ __
  \ \ /\ / / _ \| '__| |/ / __| '_ \ / _ \| '_ \
   \ V  V / (_) | |  |   <\__ \ | | | (_) | |_) |
    \_/\_/ \___/|_|  |_|\_\___/_| |_|\___/| .__/
                                          |_|
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Scale Workshop</title>
  <meta name="description" content="Scale Workshop is a tool that allows you to create microtonal tunings within your web browser. You can export these tunings to your device and use them to tune various synthesizers." />
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Open Graph -->
  <meta property="og:title" content="Scale Workshop" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://sevish.com/scaleworkshop/" />
  <meta property="og:image" content="https://sevish.com/scaleworkshop/src/assets/img/scale-workshop-og-image.png" />
  <meta property="og:description"
    content="Create microtonal tunings in your web browser." />
  <meta property="og:site_name" content="Sevish Music" />

  <!-- Styles -->
  <link rel="stylesheet" href="src/lib/bootstrap-3.3.7-dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="src/lib/jquery-ui-1.12.1/jquery-ui.min.css">
  <link rel="stylesheet" href="src/lib/socicon/style.css">
  <link rel="stylesheet" href="src/css/style.css?v=1.5">
  <link rel="stylesheet" href="src/css/style-dark.css?v=1.5">

  <!-- Favicon -->
  <link rel="apple-touch-icon" sizes="180x180" href="src/assets/favicon/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="src/assets/favicon/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="src/assets/favicon/favicon-16x16.png">
  <link rel="manifest" href="src/assets/favicon/manifest.json">
  <link rel="mask-icon" href="src/assets/favicon/safari-pinned-tab.svg" color="#9bd03b">
  <meta name="theme-color" content="#ffffff">
</head>

<body>
  <div id="splash">
    <div id="splash-center">
      <noscript>
        <img src="src/assets/img/scale-workshop-og-image.png" />
        <p class="lead" style="padding:1em;">JavaScript is disabled in your browser. Please enable JavaScript to use
          Scale Workshop.</p>
      </noscript>
    </div>
  </div>

  <div id="header-mobile" class="visible-xs-block">
    <h1 style="line-height:0.6em;text-align:center;">
      <span style="font-size:1em;">Scale</span><br/>
      <span style="font-size:0.7em;">Workshop</span>
    </h1>
  </div>

  <nav class="navbar navbar-inverse bg-inverse">
    <div class="container-fluid">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed pull-right">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <span class="navbar-brand" style="line-height:0.6em;text-align:right;">
          <span style="font-size:1em;">Scale</span><br/>
          <span style="font-size:0.7em;">Workshop</span>
        </span>
      </div>

      <div class="collapse navbar-collapse" id="mobile-menu">
        <ul class="nav navbar-nav">
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-asterisk"
                aria-hidden="true"></span> New <span class="caret"></span></a>
            <ul class="dropdown-menu">
              <li><a href="#" id="generate_equal_temperament"><span class="glyphicon glyphicon-stats"
                    aria-hidden="true"></span> Equal temperament</a></li>
              <li><a href="#" id="generate_rank_2_temperament"><span class="glyphicon glyphicon-stats"
                    aria-hidden="true"></span> Rank-2 temperament</a></li>
              <li><a href="#" id="generate_harmonic_series_segment"><span class="glyphicon glyphicon-stats"
                    aria-hidden="true"></span> Harmonic series segment</a></li>
              <li><a href="#" id="generate_subharmonic_series_segment"><span class="glyphicon glyphicon-stats"
                    aria-hidden="true"></span> Subharmonic series segment</a></li>
              <li><a href="#" id="enumerate_chord"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span>
                  Enumerate chord</a></li>
              <li><a href="#" id="generate_cps"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span> Combination product set</a></li>
              <li class="divider"></li>
              <li><a href="#" id="import-scala-scl"><span class="glyphicon glyphicon-log-in" aria-hidden="true"></span>
                  Import .scl</a></li>
              <li><a href="#" id="import-anamark-tun"><span class="glyphicon glyphicon-log-in"
                    aria-hidden="true"></span> Import .tun</a></li>
              <li><a href="#" id="import-mnlgtun-file"><span class="glyphicon glyphicon-log-in" 
                    aria-hidden="true"></span> Import .mnlgtuns / .mnlgtuno</a></li>
              <li class="divider"></li>
              <li><a href="#" id="clear-scale"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Clear scale</a></li>
              <li><a href="#" id="load-preset"><span class="glyphicon glyphicon-cd" aria-hidden="true"></span> Load
                  preset scale</a></li>
            </ul>
          </li>
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-adjust"
                aria-hidden="true"></span> Modify <span class="caret"></span></a>
            <ul class="dropdown-menu" id="modify-buttons">
              <li><a href="#" id="modify_sort_ascending"><span class="glyphicon glyphicon-sort-by-attributes" aria-hidden="true"></span> Sort ascending</a></li>
              <li><a href="#" id="modify_octave_reduce"><span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span> Reduce</a></li>
              <li><a href="#" id="modify_rotate"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Rotate</a></li>
              <li><a href="#" id="modify_mode"><span class="glyphicon glyphicon-equalizer" aria-hidden="true"></span> Subset</a></li>
              <li><a href="#" id="modify_stretch"><span class="glyphicon glyphicon-compressed"
                    aria-hidden="true"></span> Stretch/compress</a></li>
              <li><a href="#" id="modify_random_variance"><span class="glyphicon glyphicon-random"
                    aria-hidden="true"></span> Random variance</a></li>
              <li><a href="#" id="modify_sync_beating"><span class="glyphicon glyphicon-magnet"
                    aria-hidden="true"></span> Tempo-sync beating</a></li>
              <li><a href="#" id="modify_approximate"><span class="glyphicon glyphicon-list-alt"
                    aria-hidden="true"></span> Approximate by ratios</a></li>
              <li><a href="#" id="modify_approximate_harmonics"><span class="glyphicon glyphicon-list-alt"
                    aria-hidden="true"></span> Approximate by harmonics</a></li>
              <li><a href="#" id="modify_approximate_subharmonics"><span class="glyphicon glyphicon-list-alt"
                    aria-hidden="true"></span> Approximate by subharmonics</a></li>
              <li><a href="#" id="modify_equalize"><span class="glyphicon glyphicon-th"
                    aria-hidden="true"></span> Equalize</a></li>
            </ul>
          </li>
          <li><a href="#" id="nav_play"><span class="glyphicon glyphicon-music" aria-hidden="true"></span> Virtual Kbd</a>
          </li>
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-download-alt"
                aria-hidden="true"></span> Export <span class="caret"></span></a>
            <ul class="dropdown-menu" id="export-buttons">
              <li><a href="#" onclick="event.preventDefault(); export_anamark_tun(100);"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download AnaMark v1 tuning (.tun)</a>
              </li>
              <li><a href="#" onclick="event.preventDefault(); export_anamark_tun(200);"><span
                class="glyphicon glyphicon-download" aria-hidden="true"></span> Download AnaMark v2 tuning (.tun)</a>
              </li>
              <li><a href="#" onclick="event.preventDefault(); export_scala_scl();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Scala scale (.scl)</a></li>
              <li><a href="#" onclick="event.preventDefault(); export_scala_kbm();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Scala keyboard mapping (.kbm)</a>
              </li>
              <li><a href="#" onclick="event.preventDefault(); export_maxmsp_coll();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Max/MSP coll tuning
                  (.txt)</a></li>
              <li><a href="#" onclick="event.preventDefault(); export_pd_text();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download PureData text tuning
                  (.txt)</a></li>
              <li><a href="#" onclick="event.preventDefault(); export_kontakt_script();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Kontakt tuning script
                  (.txt)</a></li>
              <li><a href="#" onclick="event.preventDefault(); export_soniccouture_nka();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Soniccouture tuning file
                  (.nka)</a></li>
              <li><a href="#" onclick="event.preventDefault(); exportHarmorPitchMap();"><span class="glyphicon glyphicon-download" aria-hidden="true"></span> Download
                  Harmor pitch map (.fnv)</a></li>
              <li><a href="#" onclick="event.preventDefault(); exportSytrusPitchMap();"><span class="glyphicon glyphicon-download" aria-hidden="true"></span> Download
                  Sytrus Pitch Map (.fnv)</a></li>
              <li><a href="#" onclick="event.preventDefault(); exportMnlgtun(true);"><span class="glyphicon glyphicon-download"
                    aria-hidden="true"></span> Download Korg 'logue Sound Librarian Scale (.mnlgtuns)</a></li>
              <li><a href="#" onclick="event.preventDefault(); exportMnlgtun(false);"><span class="glyphicon glyphicon-download"
                    aria-hidden="true"></span> Download Korg 'logue Sound Librarian Octave (.mnlgtuno)</a></li>
              <li class="divider"></li>
              <li><a href="#" onclick="event.preventDefault(); export_reference_deflemask();"><span
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Deflemask 'fine tune'
                  reference (.txt)</a></li>
              <li><a href="#" id="export_reaper_note_name_map"><span 
                    class="glyphicon glyphicon-download" aria-hidden="true"></span> Download Reaper Note Name Map (.txt)
                  </a></li>
              <li class="divider"></li>
              <li><a href="#" onclick="event.preventDefault(); export_url();"><span
                    class="glyphicon glyphicon-share-alt" aria-hidden="true"></span> Share scale as URL</a></li>
            </ul>
          </li>
          <li><a href="#" id="nav_midi"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> MIDI I/O</a>
          <li class="dropdown">
            <a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-info-sign"
                aria-hidden="true"></span> About <span class="caret"></span></a>
            <ul class="dropdown-menu">
              <li><a href="#" id="about_scale_workshop"><img src="src/assets/favicon/android-chrome-192x192.png"
                    style="height:1em; width:auto;" /> About Scale Workshop</a></li>
              <li><a href="https://github.com/SeanArchibald/scale-workshop" target="_blank"><span
                    class="glyphicon glyphicon-book" aria-hidden="true"></span> GitHub</a></li>
              <li><a href="https://github.com/SeanArchibald/scale-workshop/issues" target="_blank"><span
                    class="glyphicon glyphicon-send" aria-hidden="true"></span> Report issue</a></li>
              <li><a href="http://sevish.com" target="_blank"><span class="glyphicon glyphicon-heart-empty"
                    aria-hidden="true"></span> Made by Sevish</a></li>
            </ul>
          </li>
          <li><a href="guide.htm" target="_blank"><span class="glyphicon glyphicon-question-sign"
                aria-hidden="true"></span> User guide</a></li>
        </ul>
      </div>
    </div>
  </nav>
  <div class="container-fluid">

    <div class="row">
      <div class="col-sm-6 col-md-4 col-lg-4 col-main">
        <form>
          <div class="form-group">
            <textarea id="txt_name" name="name" placeholder="Untitled scale" class="form-control" rows="1"></textarea>
          </div>

          <label>Scale data <a href="guide.htm#manual-data-entry" target="_blank"><span
                class="glyphicon glyphicon-question-sign" aria-hidden="true"></span></a></label>
          <div class="form-group">
            <textarea id="txt_tuning_data" class="form-control" rows="12"
              title="Enter one interval per line. Values with a . will be calculated as cents. Values with a backslash (n\m) are n degrees of m-EDO. Values with forward slash (/) are calculated as ratios. See the user guide for examples (About > User Guide)"
              placeholder=" "></textarea>
          </div>

          <canvas id="graphic-scale-rule" width="1000px" height="100px"></canvas>

          <div class="form-group">
            <label>Base frequency (note 1/1)</label>
            <div class="input-group">
              <input id="txt_base_frequency" name="base_frequency" type="number" min="0.0001" max="999999"
                class="form-control" value="440" title="Set the frequency (in Hz) of note 1/1" />
              <div class="input-group-addon">Hz</div>
            </div>
            <input name="Auto" id="btn_frequency_auto" type="button" value="Auto" class="btn btn-default pull-right"
              title="Automatically set base frequency from the MIDI note, assuming 12-EDO A440Hz" />
          </div>

          <div class="form-group">
            <label>Base MIDI note (note 1/1)</label>
            <div class="input-group"
              title="Set the MIDI note number to be note 1/1. By default this is 69 (A above middle C).">
              <input id="txt_base_midi_note" name="base_midi_note" type="number" min="0" max="127" step="1"
                class="form-control" value="69" />
              <div id="base_midi_note_name" class="input-group-addon" style="min-width:5em;">A5</div>
            </div>
          </div>
        </form>
      </div>

      <div class="col-sm-6 col-md-8 col-lg-8 col-main">
        <div class="row">

          <!-- Settings column -->
          <div class="col-sm-12 col-md-6 col-md-push-6 col-lg-6 col-sub">

            <a id="btn_panic" href="#" class="btn btn-link" style="float:right;margin-top:1em;"
              title="Stops all currently playing notes and also turns down the Delay feedback gain.">Quiet</a>

            <div id="qwerty-indicator"></div>

            <div id="settings-accordion">

              <!-- general settings -->
              <div id="settings_general" class="group">
                <h3>General settings</h3>
                <div>
                  <form>
                    <div class="form-group">
                      <label>Line endings format</label>
                      <select id="input_select_newlines" class="form-control"
                        title="If your exported tuning files didn't work right on macOS synths, try changing this option to Unix.">
                        <option value="windows">Microsoft (Windows/MS-DOS)</option>
                        <option value="unix">Unix (Mac/Linux)</option>
                      </select>
                    </div>
                    <div class="form-group">

                      <div class="checkbox">
                        <label>
                          <input id="input_checkbox_night_mode" type="checkbox" /> <label for="input_checkbox_night_mode">Dark mode</label>
                        </label>
                      </div>
                    </div>
                  </form>
                </div>
              </div>

              <!-- synth settings -->
              <div id="settings_synth" class="group">
                <h3>Synth settings</h3>
                <div>
                  <form>
                    <div class="form-group">
                      <label for="input_range_main_vol">Main Volume</label>
                      <input type="range" class="form-control-range" id="input_range_main_vol" min="0" max="1"
                        step="0.005" value="0.8">
                    </div>
                    <div class="form-group">
                      <label>Waveform</label>
                      <select id="input_select_synth_waveform" class="form-control">
                        <option value="semisine">Semisine</option>
                        <option value="octaver">Octaver</option>
                        <option value="triangle">Triangle</option>
                        <option value="square">Square</option>
                        <option value="sawtooth">Sawtooth</option>
                        <option value="brightness">Brightness</option>
                        <option value="harmonicbell">Harmonic Bell</option>
                        <option value="warm1">Warm 1</option>
                        <option value="warm2">Warm 2</option>
                        <option value="warm3">Warm 3</option>
                        <option value="warm4">Warm 4</option>
                        <option value="sine">Sine</option>
                      </select>
                    </div>
                    <div class="form-group">
                      <label>Amplitude Envelope</label>
                      <select id="input_select_synth_amp_env" class="form-control">
                        <option value="organ">Organ</option>
                        <option value="pad">Pad</option>
                        <option value="perc-short">Percussive (Short)</option>
                        <option value="perc-medium">Percussive (Medium)</option>
                        <option value="perc-long">Percussive (Long)</option>
                      </select>
                    </div>
                    <hr />
                    <div class="form-group">
                      <label>Delay effect</label>
                      <div class="checkbox">
                        <label>
                          <input id="input_checkbox_delay_on" type="checkbox" /> On
                        </label>
                      </div>
                    </div>
                    <div class="form-group">
                      <label for="input_range_feedback_gain">Feedback gain</label>
                      <input type="range" class="form-control-range" id="input_range_feedback_gain" min="0" max="0.99"
                        step="0.005" value="0.3">
                    </div>
                    <div class="form-group">
                      <label for="input_range_delay_time">Delay Time (<span id="delay_time_ms">400</span> ms)</label>
                      <input type="range" class="form-control-range" id="input_range_delay_time" min="10" max="5000"
                        value="400">
                    </div>
                    <hr/>
                    <div class="form-group">
                      <label>Max polyphony</label>
                      <p>Refresh page for changes to take effect.</p>
                      <input title="Maximum number of notes that can be played at the same time (default 16). More system resources are required for greater polyphony. Changes will take effect next time the page is refreshed." id="input_number_max_polyphony" type="number" class="form-control" value="16" min="1" max="64" placeholder="16" />
                    </div>
                  </form>
                </div>
              </div>

              <!-- Isomorphic keyboard settings -->
              <div id="settings_note_input" class="group">
                <h3>Isomorphic keyboard settings</h3>
                <div>
                  <p>Play your scale using your computer keyboard or the virtual keyboard.</p>
                  <form>

                    <div class="form-group">
                      <label>Computer keyboard layout</label>
                      <select id="input_select_keyboard_layout" class="form-control"
                        title="Select your regional keyboard layout.">
                        <option value="EN">English (QWERTY)</option>
                        <option value="HU">Hungarian (QWERTZ)</option>
                        <option value="DK">Dvorak</option>
                        <option value="PK">Programmer Dvorak</option>
                        <option value="CO">Colemak</option>
                        <option value="CO_DH">Colemak DH</option>
                      </select>
                    </div>
                    <div class="form-group">
                      <label>Isomorphic key mapping</label>
                      <p>Distance (in scale degrees) between adjacent keys on the horizontal/vertical axes.</p>
                      <div class="input-group">
                        <div class="input-group-addon">V</div>
                        <input id="input_number_isomorphicmapping_vert" type="number" class="form-control" value="5"
                          min="-100" max="100" />
                        <div class="input-group-addon">default 5</div>
                      </div>
                      <div class="input-group">
                        <div class="input-group-addon">H</div>
                        <input id="input_number_isomorphicmapping_horiz" type="number" class="form-control" value="1"
                          min="-100" max="100" />
                        <div class="input-group-addon">default 1</div>
                      </div>
                    </div>
                    <div class="form-group">
                      <label>Key colours</label>
                      <p>A list of key colours, ascending from 1/1. Key colours are purely cosmetic and do not affect
                        mapping.</p>
                      <textarea id="input_key_colors" class="form-control"
                        placeholder="white black white white black white black white white black white black"
                        title="Examples: black grey white red green blue palevioletred antiquewhite steelblue olive">white black white white black white black white white black white black</textarea>
                      <input name="Auto" id="btn_key_colors_auto" type="button" value="Auto"
                        class="btn btn-default pull-right"
                        title="Automatically colour keys based on your scale size. Auto colouring is not optimal but a good starting point." />
                    </div>
                  </form>
                </div>
              </div>
            </div>
          </div>

          <!-- tuning table column -->
          <div id="col-tuning-table" class="col-sm-12 col-md-6 col-md-pull-6 col-lg-6 col-sub">
            <table id="tuning-table" class="table table-condensed table-hover">
            </table>
          </div>

        </div><!-- /.row -->
      </div><!-- /col -->
    </div><!-- /.row -->
  </div><!-- /.container-fluid -->

  <!-- Hidden by default -->
  <input type="file" id="scala-file" accept=".scl" style="display:none;" onchange="parse_imported_scala_scl(event)" />
  <input type="file" id="anamark-tun-file" accept=".tun" style="display:none;"
    onchange="parse_imported_anamark_tun(event)" />
  <input type="file" id="mnlgtun-file" accept=".mnlgtuns,.mnlgtuno" style="display:none;" 
    onchange="parseImportedMnlgtun(event)" />



  <!-- Generator Modals -->
  <div id="modal_generate_equal_temperament" class="modal" title="Generate equal temperament">
    <a href="guide.htm#equal-temperaments" target="_blank"><span class="glyphicon glyphicon-question-sign"
        aria-hidden="true" style="float:right;"></span></a>
    <form>
      <label>Number of divisions</label>
      <div class="form-group">
        <input id="input_number_of_divisions" name="" type="number" min="1" max="999999" class="form-control"
          value="5" />
      </div>

      <label>Interval to divide</label>
      <div class="form-group">
        <input id="input_interval_to_divide" name="" type="text" class="form-control" value="2/1" />
      </div>
    </form>
  </div>

  <div id="modal_generate_rank_2_temperament" class="modal" title="Generate rank-2 temperament">
    <form>
      <label>Generator</label>
      <div class="form-group">
        <input id="input_rank-2_generator" name="" type="text" class="form-control" value="3/2" />
      </div>

      <label>Period</label>
      <div class="form-group">
        <input id="input_rank-2_period" name="" type="text" class="form-control" value="2/1" />
      </div>

      <label>Scale size</label>
      <div class="form-group">
        <input id="input_rank-2_size" name="" type="number" min="2" max="99999" step="1" class="form-control"
          value="7" />
      </div>

      <label>Generators up/down from 1/1</label>
      <div class="form-group form-inline">
        <div class="input-group">
          <input id="input_rank-2_up" name="" type="number" min="0" max="6" step="1" class="form-control" value="6"
            title="Number of generators up" style="min-width:5em;" />
          <div class="input-group-addon">Up</div>
        </div>
        <div class="input-group">
          <input id="input_rank-2_down" name="" type="number" min="0" max="99999" step="1" class="form-control"
            value="0" disabled />
          <div class="input-group-addon">Down</div>
        </div>
      </div>
    </form>
    <hr />
    <button class="btn btn-default"
      onclick="show_mos_cf( jQuery('#input_rank-2_period').val(), jQuery('#input_rank-2_generator').val(), jQuery('#input_rank-2_size').val(), jQuery('#input_rank-2_mos_threshold').val() );"
      style="float:left;">Show MOS</button>
    <p style="text-align:right;">(ignore MOS with steps smaller than <input id="input_rank-2_mos_threshold" name=""
        type="number" min="0.1" max="600" value="2.5" style="width:4em;" /> cents)</p>
    <span id="info_rank_2_mos" style="display:block;margin-top:10px;"></span>
    <hr />
    <label>Output type:</label>
      <select id="input_rank-2_type" name="" class="form-control" value="preserve">
        <option value="preserve">Preserve types</option>
        <option value="cents">Cents</option>
        <option value="decimals">Decimals</option>
      </select>
  </div>

  <div id="modal_generate_harmonic_series_segment" class="modal" title="Generate harmonic series segment">
    <form>
      <label>Lowest harmonic</label>
      <div class="form-group">
        <input id="input_lowest_harmonic" name="" type="number" min="1" max="999999" step="1" class="form-control"
          value="8" />
      </div>

      <label>Highest harmonic</label>
      <div class="form-group">
        <input id="input_highest_harmonic" name="" type="number" min="1" max="999999" step="1" class="form-control"
          value="16" />
      </div>
    </form>
  </div>

  <div id="modal_generate_subharmonic_series_segment" class="modal" title="Generate subharmonic series segment">
    <form>
      <label>Lowest subharmonic</label>
      <div class="form-group">
        <input id="input_lowest_subharmonic" name="" type="number" min="1" max="999999" step="1" class="form-control"
          value="8" />
      </div>

      <label>Highest subharmonic</label>
      <div class="form-group">
        <input id="input_highest_subharmonic" name="" type="number" min="1" max="999999" step="1" class="form-control"
          value="16" />
      </div>
    </form>
  </div>

  <div id="modal_enumerate_chord" class="modal" title="Enumerate chord">
    <form>
      <label>Chord</label>
      <div class="form-group">
        <input id="input_chord" name="" type="text" class="form-control" value="4:5:6:7:8" />
      </div>
      <div class="form-group">
        <label>
          <input id="input_invert_chord" type="checkbox" /> Invert chord
        </label>
      </div>
      <label>
        <input id="input_convert_to_ratios" type="checkbox" /> Convert all to ratios
      </label>
    </form>
  </div>

  <div id="modal_generate_cps" class="modal" title="Generate combination product set">
    <form>
      <label>Factors</label>
      <div class="form-group">
        <input id="input_cps_factors" name="" type="text" min="1" max="999999" step="1" class="form-control"
          placeholder="1 3 5 7" list="input_cps_factors_list" />
        <datalist id="input_cps_factors_list">
          <option value="1 3 5 7" />
          <option value="1 3 7 9" />
          <option value="3 5 7 9" />
          <option value="7 9 11 15" />
          <option value="1 3 5 121" />
          <option value="1 3 5 7 9" />
          <option value="2 3 5 7 11" />
          <option value="1 3 5 7 9 11" />
          <option value="2 3 5 7 11 13" />
        </datalist>   
      </div>

      <label>Combination count</label>
      <div class="form-group">
        <input id="input_cps_combination_count" name="" type="number" pattern="[0-9]" min="2" max="9" step="1" class="form-control"
          value="2" />
      </div>

      <div class="form-group">
        <label>
          <input id="input_cps_remove_1" type="checkbox" checked="checked" /> Remove 1/1 from scale
        </label>
      </div>

      <div class="form-group">
        <label>
          <input id="input_cps_reduce" type="checkbox" checked="checked" /> Reduce resulting scale by 2/1
        </label>
      </div>
    </form>
  </div>

  <div id="modal_load_preset_scale" class="modal" title="Load preset scale">
    <form>
      <div class="form-group">
        <select id="select_preset_scale" name="" size="10" class="form-control">
          <optgroup label="Traditional scales">
            <option value="pelog">Pelog</option>
            <option value="slendro">Slendro</option>
            <option value="ragabageshri">Raga Bageshri</option>
            <option value="ragabhairavi">Raga Bhairavi</option>
            <option value="ragakafi">Raga Kafi</option>
            <option value="ragatodi">Raga Todi</option>
            <option value="ragayaman">Raga Yaman</option>
            <option value="22shruti">22 Shruti</option>
            <option value="hirajoshi">Hirajoshi</option>
            <option value="balafon">Balafon 1</option>
            <option value="balafon2">Balafon 2</option>
            <option value="balafon3">Balafon 3</option>
            <option value="balafon4">Balafon 4</option>
            <option value="balafon5">Balafon 5</option>
            <option value="balafon6">Balafon 6</option>
            <option value="balafon7">Balafon 7</option>
            <option value="5edo">Equal pentatonic</option>
            <option value="7edo">Equal heptatonic</option>
            <option value="archytasdiatonic">Archytas Diatonic</option>
            <option value="archytasenharmonic">Archytas Enharmonic</option>
            <option value="didymuschromatic">Didymus Chromatic</option>
            <option value="ptolemydiatonicditoniaion">Ptolemy Diatonic Ditoniaion</option>
            <option value="ptolemydiatonichemiolion">Ptolemy Diatonic Hemiolion</option>
            <option value="pythagorean">Pythagorean</option>
            <option value="werckmeisteriii">Werckmeister III (1691)</option>
            <option value="young1799">Young (1799)</option>
            <option value="12edo">12-tone equal temperament</option>
          </optgroup>
          <optgroup label="Just intonation scales">
            <option value="partch43">Harry Partch 43-tone</option>
            <option value="carlossuperjust">Wendy Carlos Super Just</option>
            <option value="gradycentaur">Kraig Grady Centaur (7-limit)</option>
            <option value="gradycentauras">Kraig Grady Centaura Subharmonic (11-limit)</option>
            <option value="gradycentaurah">Kraig Grady Centaura Harmonic (11-limit)</option>
          </optgroup>
          <optgroup label="Equal temperament subsets">
            <option value="11machine6">11edo machine[6]</option>
            <option value="13glacial7">13edo glacial[7]</option>
            <option value="13father8">13edo father[8]</option>
            <option value="15blackwood10">15edo blackwood[10]</option>
            <option value="16mavila7">16edo mavila[7]</option>
            <option value="17superpyth12">17edo superpyth[12]</option>
            <option value="17rast">17edo Rast</option>
            <option value="22porcupine8">22edo porcupine[8]</option>
            <option value="22orwell9">22edo orwell[9]</option>
            <option value="22pajara12">22edo pajara[12]</option>
            <option value="26lemba10">26edo lemba[10]</option>
            <option value="26flattone12">26edo flattone[12]</option>
            <option value="31meantone19">31edo meantone[19]</option>
            <option value="46sensi11">46edo sensi[11]</option>
            <option value="313island9">313edo island[9]</option>
          </optgroup>
          <optgroup label="Non-octave scales">
            <option value="bohlenpierceeq">Bohlen-Pierce equal (13ed3)</option>
            <option value="bohlenpierceji">Bohlen-Pierce just</option>
            <option value="carlosalpha">Wendy Carlos Alpha</option>
            <option value="carlosbeta">Wendy Carlos Beta</option>
            <option value="carlosgamma">Wendy Carlos Gamma</option>
            <option value="65cet">65 cent Equal Temperament</option>
            <option value="88cet">88 cent Equal Temperament</option>
          </optgroup>
        </select>
      </div>
    </form>
  </div>

  <!-- Modify Modals -->
  <div id="modal_modify_stretch" class="modal" title="Stretch/compress tuning">
    <p>This applies a stretching or compression evenly across the whole scale.<br />Entering 1 will cause no change;
      entering 2 will make every interval twice as large.</p>
    <form>
      <label>Stretch ratio</label>
      <div class="form-group">
        <input id="input_stretch_ratio" name="" type="number" class="form-control" value="1.005" min="0.0001"
          max="99999" />
      </div>
    </form>
  </div>

  <div id="modal_modify_random_variance" class="modal" title="Random variance">
    <p>This will add a random amount of detuning to each note of the scale.</p>
    <form>
      <label>Maximum variance in cents</label>
      <div class="form-group">
        <input id="input_cents_max_variance" name="" type="number" class="form-control" value="10" min="0"
          max="99999" />
      </div>

      <div class="checkbox"
        title="By default, the 'octave' won't be detuned. This prevents the scale sound progressively more out-of-tune as you go up/down by octaves. Check this box option to over-ride this.">
        <label>
          <input id="input_checkbox_vary_period" type="checkbox" /> Vary the octave
        </label>
      </div>
    </form>
  </div>

  <div id="modal_modify_mode" class="modal" title="Subset">
    <p>Select a subset from the current scale.</p>
    <form>
      <label>Mode</label>
      <div class="form-group">
        <div class="input-group">
          <input id="input_modify_mode" name="" type="text" class="form-control" value="" placeholder="2 2 1 2 2 2 1" />
          <div class="input-group-addon">
            <span id="input_modify_mode_counter"></span>
          </div>
        </div>
        <div class="form-row">
          <button class="btn btn-default" id="input_mode_step_left" type="button">&laquo</button>
          <button class="btn btn-default" id="input_mode_step_right" type="button">&raquo</button>
        </div>
        <div class="radio">
          <label><input type="radio" name="mode_type" value="intervals" checked>Intervals (e.g. 2 2 1 2 2 2 1)</label>
        </div>
        <div class="radio">
          <label><input type="radio" name="mode_type" value="frombase">From base note (e.g. 2 4 5 7 9 11 12)</label>
        </div>
        <div class="radio">
          <label><input type="radio" name="mode_type" value="mos">MOS Mode (select a degree to stack)</label>
        </div>
        <div id="mos_mode_options">
          <div class="form-row">
            <div class="form-group col-md-6">
              <label> Degree </label>
              <select class="form-control" id="modal_modify_mos_degree"></select>
            </div>
            <div class="form-group col-md-6">
              <label> Size </label>
              <select class="form-control" id="modal_modify_mos_size"></select>
            </div>
          </div>
        </div>
      </div>
    </form>
  </div>

  <div id="modal_modify_sync_beating" class="modal" title="Tempo-sync beating">
    <a href="guide.htm#tempo-sync-beating" target="_blank"><span class="glyphicon glyphicon-question-sign"
        aria-hidden="true" style="float:right;"></span></a>
    <form>
      <label>BPM</label>
      <div class="form-group">
        <input id="input_modify_sync_beating_bpm" name="" type="number" class="form-control" value="100"
          placeholder="100" />
      </div>

      <label>Resolution</label>
      <div class="form-group">
        <select id="select_sync_beating_resolution" name="" class="form-control">
          <option value="16">16 (2^4)</option>
          <option value="24">24 (2^3 . 3)</option>
          <option value="27">27 (3^3)</option>
          <option value="30">30 (2 . 3 . 5)</option>
          <option value="32">32 (2^5)</option>
          <option value="48">48 (2^4 . 3)</option>
          <option value="64" selected>64 (2^6)</option>
          <option value="96">96 (2^5 . 3)</option>
          <option value="128">128 (2^7)</option>
          <option value="144">144 (2^4 . 3^2)</option>
          <option value="192">192 (2^6 . 3)</option>
          <option value="256">256 (2^8)</option>
          <option value="512">512 (2^9)</option>
          <option value="1024">1024 (2^10)</option>
          <option value="2048">2048 (2^11)</option>
          <option value="2310">2310 (2 . 3 . 5 . 7 . 11)</option>
        </select>
      </div>
    </form>
  </div>

  <div id="modal_approximate_intervals" class="modal" title="Approximate by ratios">
    <p>Select a ratio that approximates the interval.</p>
    <form>
      <label>Scale Degree</label>
      <div class="form-group">
        <input id="input_scale_degree" name="" type="number" class="form-control" value="1" min="1" max="128" />
      </div>
      <label>Interval</label>
      <div class="form-group">
        <input id="input_interval_to_approximate" name="" tyep="number" class="form-control" value="" />
      </div>
      <div class="form-group">
        <label>Approximation</label>
        <select class="form-control" id="approximation_selection"></select>
      </div>
      <div class="form-group">
        <label>
          <input id="input_show_convergents" type="checkbox"> Only show convergents</input>
        </label>
      </div>
      <div class="form-row">
        <div class="form-group col-md-6">
          <label>Min Error</label>
          <input id="input_min_error" name="" type="number" class="form-control" value="0.0" min="0"
            max="200.0">cents</input>
        </div>
        <div class="form-group col-md-6">
          <label>Max Error</label>
          <input id="input_max_error" name="" type="number" class="form-control" value="15.0" min="1.0"
            max="200.0">cents</input>
        </div>
      </div>
      <div class="form-row">
        <div class="form-group col-md-6">
          <label>Min Prime Limit</label>
          <input id="input_approx_min_prime" name="" type="number" class="form-control" value="2" min="0"
            max="7919" />
        </div>
        <div class="form-group col-md-6">
          <label>Max Prime Limit</label>
          <input id="input_approx_max_prime" name="" type="number" class="form-control" value="31" min="0"
            max="7919" />
        </div>
      </div>
    </form>
  </div>

  <div id="modal_approximate_harmonics" class="modal" title="Approximate by harmonics">
    <form>
      <label>Denonimator</label>
      <div class="form-group">
        <input id="input_approx_harm_denominator" name="" type="number" class="form-control" value="128" min="1" max="1000000" />
      </div>
    </form>
  </div>

  <div id="modal_approximate_subharmonics" class="modal" title="Approximate by subharmonics">
    <p>Note: if you enter an odd number, the interval of equivalence will change (e.g. mistuned octaves)</p>
    <form>
      <label>Numerator</label>
      <div class="form-group">
        <input id="input_approx_subharm_numerator" name="" type="number" class="form-control" value="128" min="1" max="1000000" />
      </div>
    </form>
  </div>

  <div id="modal_equalize" class="modal" title="Equalize">
    <p>Divides your interval of equivalence into an equal number of steps, then rounds each interval in your scale to the nearest equal step.</p>
    <form>
      <label>Equal divisions</label>
      <div class="form-group">
        <input id="input_equalize_divisions" name="" type="number" class="form-control" value="" min="1" max="1000000" />
      </div>
    </form>
  </div>

  <div id="modal_modify_octave_reduce" class="modal" title="Reduce">
    <p>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.</p>
    <form>
      <label>Modulus</label>
      <div class="form-group">
        <input id="input_reduce_octave" name="" type="text" class="form-control" value="2/1" placeholder="2/1" />
      </div>
      <div class="form-group">
        <label>
          <input id="input_reduce_also_sort" type="checkbox" checked="checked" /> Sort resulting scale ascendingly
        </label>
      </div>
    </form>
  </div>

  <div id="modal_modify_rotate" class="modal" title="Rotate">
    <p>Rotates the mode of your scale.</p>
    <p>The resulting scale will be sorted ascendingly.</p>
    <form>
      <label>New 1/1</label>
      <div class="form-group">
        <select id="input_rotate_new_1_1" class="form-control">
        </select>
      </div>
    </form>
  </div>

  <!-- Export Modals -->
  <div id="modal_share_url" class="modal" title="Share scale as URL">
    <p>Share your scale using the sharing link.</p>
    <form>
      <div class="form-group">
        <input id="input_share_url" name="" type="text" class="form-control" value="" readonly />
      </div>
    </form>
    <p class="social-icons">
      <a class="social-icons-email" href="#" title="Send scale via email"><span class="socicon-mail"></span></a>
      <a class="social-icons-twitter" href="#" title="Tweet this scale"><span class="socicon-twitter"></span></a>
    </p>
  </div>
  <div id="modal_reaper_named_notes" class="modal" title="Reaper Note Name Map">
    <p>Select the options for the note map.</p>
    <form>
        <div class="form-group">
            <label>Pitch Format:</label> 
            <select id="input_reaper_pitch_format" class="form-control">
                <option value="scale data">Scale Data</option>
                <option value="cents">Cents</option>    
                <option value="freq">Frequencies</option>
                <option value="decimal">Decimal Ratio</option>
                <option value="degree">Scale Degree</option>
            </select>
        </div>
        <div class="form-group">
            <label>Period options:</label>
            <div class="checkbox">
              <label title="Show the number of periods away from the base note next to the pitch">
                <input id="input_reaper_show_period_numbers" type="checkbox" name="period-format" checked> Show period number</input>
              </label>
            </div>
            <div class="checkbox">
              <label title="Transpose the scale intervals by the period across the whole range of notes">
                <input id="input_reaper_calculate_periods" type="checkbox" name="period-format"> Calculate period in pitch</input>
              </label>
            </div>
        </div>
        <div id="modal_reaper_root_period_group" class="form-group">
            <label title="The number of periods from which the base note is offset">Base Period Number: </label>
            <input id="input_reaper_root_period" type="number" class="form-control" value="0"></input>
        </div>
        <div id="modal_reaper_root_cents_group" class="form-group">
            <label title="An offset of cents at which the base note set">Base Cents Value: </label>
            <input id="input_reaper_root_cents" type="number" class="form-control" value="0.0"></input>
        </div>
        <div id="modal_reaper_root_degree_group" class="form-group">
            <label title="An offset of degrees at which the base note is set">Base Degree Value: </label>
            <input id="input_reaper_root_degree" type="number" class="form-control" value="0"></input>
        </div>
    </form>
  </div>

  <!-- MIDI Settings modal -->
  <div id="modal_midi_settings" class="modal" title="MIDI settings">
    <div style="display:flex">
      <div class="form-group" style="flex-grow:1">
        <label for="midi-enabler">MIDI</label>
        <button type="button" class="btn btn-danger" id="midi-enabler">off</button>
      </div>
      <div class="form-group">
        <button type="button" class="btn btn-success" id="velocity-toggler">velocity: on</button>
      </div>
    </div>
    <form>
      <div class="form-group">
        <label>Input device ports</label>
      </div>
      <div class="inputs"></div>

      <div class="form-group">
        <label>Output device ports</label>
      </div>
      <div class="outputs"></div>

      <div class="form-group">
        <label>Other settings</label>
      </div>
      <div class="settings">
        <div class="row">
          <div class="checkbox-wrapper">
            <input id="input_midi_whitemode" type="checkbox" />
          </div>
          <label for="input_midi_whitemode">Map to white keys only</label>
        </div>
      </div>
    </form>
  </div>

  <!-- About Scale Workshop modal -->
  <div id="modal_about_scale_workshop" class="modal text-center" title="About Scale Workshop">
    <img src="src/assets/favicon/android-chrome-192x192.png" style="margin-top: 2em;" />
    <h2 id="about_version"></h2>
    <p><em>Because there are more than 12 notes</em></p>
    <hr />
    <p>Sevish, Lajos Mészáros, Vincenzo Sicurella, Lumi Pakkanen, Scott Thompson, Carl Lumma, Tobia, Azorlogh</p>
  </div>

  <!-- Play virtual keyboard -->
  <table id="virtual-keyboard">
    <tr class="hidden-xs hidden-sm">
      <td data-coord="[3,0]"></td>
      <td data-coord="[3,1]"></td>
      <td data-coord="[3,2]"></td>
      <td data-coord="[3,3]"></td>
      <td data-coord="[3,4]"></td>
      <td data-coord="[3,5]"></td>
      <td data-coord="[3,6]"></td>
      <td data-coord="[3,7]"></td>
      <td data-coord="[3,8]"></td>
      <td data-coord="[3,9]"></td>
      <td data-coord="[3,10]"></td>
      <td data-coord="[3,11]"></td>
      <td data-coord="[3,12]"></td>
    </tr>
    <tr>
      <td data-coord="[2,0]"></td>
      <td data-coord="[2,1]"></td>
      <td data-coord="[2,2]"></td>
      <td data-coord="[2,3]"></td>
      <td data-coord="[2,4]"></td>
      <td data-coord="[2,5]"></td>
      <td data-coord="[2,6]"></td>
      <td data-coord="[2,7]"></td>
      <td data-coord="[2,8]"></td>
      <td data-coord="[2,9]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[2,10]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[2,11]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[2,12]" class="hidden-xs hidden-sm"></td>
    </tr>
    <tr>
      <td data-coord="[1,0]"></td>
      <td data-coord="[1,1]"></td>
      <td data-coord="[1,2]"></td>
      <td data-coord="[1,3]"></td>
      <td data-coord="[1,4]"></td>
      <td data-coord="[1,5]"></td>
      <td data-coord="[1,6]"></td>
      <td data-coord="[1,7]"></td>
      <td data-coord="[1,8]"></td>
      <td data-coord="[1,9]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[1,10]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[1,11]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[1,12]" class="hidden-xs hidden-sm"></td>
    </tr>
    <tr>
      <td data-coord="[0,0]"></td>
      <td data-coord="[0,1]"></td>
      <td data-coord="[0,2]"></td>
      <td data-coord="[0,3]"></td>
      <td data-coord="[0,4]"></td>
      <td data-coord="[0,5]"></td>
      <td data-coord="[0,6]"></td>
      <td data-coord="[0,7]"></td>
      <td data-coord="[0,8]"></td>
      <td data-coord="[0,9]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[0,10]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[0,11]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[0,12]" class="hidden-xs hidden-sm"></td>
    </tr>
    <tr>
      <td data-coord="[-1,0]"></td>
      <td data-coord="[-1,1]"></td>
      <td data-coord="[-1,2]"></td>
      <td data-coord="[-1,3]"></td>
      <td data-coord="[-1,4]"></td>
      <td data-coord="[-1,5]"></td>
      <td data-coord="[-1,6]"></td>
      <td data-coord="[-1,7]"></td>
      <td data-coord="[-1,8]"></td>
      <td data-coord="[-1,9]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[-1,10]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[-1,11]" class="hidden-xs hidden-sm"></td>
      <td data-coord="[-1,12]" class="hidden-xs hidden-sm"></td>
    </tr>
    <tr class="hidden-xs hidden-sm">
      <td data-coord="[-2,0]"></td>
      <td data-coord="[-2,1]"></td>
      <td data-coord="[-2,2]"></td>
      <td data-coord="[-2,3]"></td>
      <td data-coord="[-2,4]"></td>
      <td data-coord="[-2,5]"></td>
      <td data-coord="[-2,6]"></td>
      <td data-coord="[-2,7]"></td>
      <td data-coord="[-2,8]"></td>
      <td data-coord="[-2,9]"></td>
      <td data-coord="[-2,10]"></td>
      <td data-coord="[-2,11]"></td>
      <td data-coord="[-2,12]"></td>
    </tr>
  </table>

  <!-- Dependencies -->
  <script src="src/lib/jquery-3.2.1.min.js"></script>
  <script src="src/lib/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
  <script src="src/lib/jquery-ui-1.12.1/external/jquery/jquery.js"></script>
  <script src="src/lib/jquery-ui-1.12.1/jquery-ui.min.js"></script>
  <script src="src/lib/eventemitter3.js"></script>
  <script src="src/lib/ramda-0.27.1.min.js"></script>
  <script src="src/lib/jszip.min.js"></script>
  <script src="src/lib/decimal.js"></script>
  <script src="src/lib/webmidi-3.0.19.iife.min.js"></script>

  <!-- Scale Workshop scripts -->
  <script src="src/js/state/state.js?v=1.5"></script>
  <script src="src/js/constants.js?v=1.5"></script>
  <script src="src/js/helpers.js?v=1.5"></script>
  <script src="src/js/scaleworkshop.js?v=1.5"></script>
  <script src="src/js/ui.js?v=1.5"></script>
  <script src="src/js/generators.js?v=1.5"></script>
  <script src="src/js/modifiers.js?v=1.5"></script>
  <script src="src/js/exporters.js?v=1.5"></script>
  <script src="src/js/graphics.js?v=1.5"></script>
  <script src="src/js/keymap.js?v=1.5"></script>
  <script src="src/js/synth/Voice.js?v=1.5"></script>
  <script src="src/js/synth/Delay.js?v=1.5"></script>
  <script src="src/js/synth/Synth.js?v=1.5"></script>
  <script src="src/js/synth.js?v=1.5"></script>

  <script src="src/js/midi/constants.js"></script>
  <script src="src/js/midi/commands.js?v=1.5"></script>
  <script src="src/js/midi/math.js?v=1.5"></script>
  <script src="src/js/midi/ui.js?v=1.5"></script>
  <script src="src/js/midi/midi.js?v=1.5"></script>

  <script src="src/js/events.js?v=1.5"></script>
  <script src="src/js/state/reactions.js?v=1.5"></script>
  <script src="src/js/state/reactions-dom.js?v=1.5"></script>
  <script src="src/js/state/actions.js?v=1.5"></script>
  <script src="src/js/state/actions-dom.js?v=1.5"></script>
  <script src="src/js/state/on-ready.js?v=1.5"></script>
  <script src="src/js/user.js?v=1.5"></script>
</body>
</html>

================================================
FILE: src/assets/favicon/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="/mstile-150x150.png"/>
            <TileColor>#00a300</TileColor>
        </tile>
    </msapplication>
</browserconfig>


================================================
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('<option value="' + i + '">' + scale[i] + '</option>')
    }
    // 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('<option value="' + num + '">' + text + '</option>')
    }
  }

  // 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('<option value="' + size + '">' + size + '</option>')
    }
  })

  // 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) => 
Download .txt
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
Download .txt
SYMBOL INDEX (404 symbols across 23 files)

FILE: src/js/constants.js
  constant LINE_TYPE (line 1) | const LINE_TYPE = {
  constant SEMITONE_RATIO_IN_12_EDO (line 9) | const SEMITONE_RATIO_IN_12_EDO = Math.pow(2, 1 / 12)
  constant MNLG_OCTAVESIZE (line 11) | const MNLG_OCTAVESIZE = 12
  constant MNLG_SCALESIZE (line 12) | const MNLG_SCALESIZE = 128
  constant MNLG_MAXCENTS (line 13) | const MNLG_MAXCENTS = 12800
  constant MNLG_A_REF (line 14) | const MNLG_A_REF = { val: 6900, ind: 69, freq: 440.0 }
  constant MNLG_C_REF (line 15) | const MNLG_C_REF = { val: 6000, ind: 60, freq: 261.6255653 }
  constant PRIMES (line 18) | const PRIMES = [

FILE: src/js/events.js
  function show_modify_mode_mos_options (line 378) | function show_modify_mode_mos_options(showOptions) {
  function update_modify_mode_mos_generators (line 388) | function update_modify_mode_mos_generators() {
  function modify_mode_update_mos_scale (line 401) | function modify_mode_update_mos_scale() {

FILE: src/js/exporters.js
  function export_error (line 1) | function export_error() {
  function save_file (line 9) | function save_file(filename, contents, raw, mimeType = 'application/octe...
  function export_anamark_tun (line 23) | function export_anamark_tun(version) {
  function export_scala_scl (line 137) | function export_scala_scl() {
  function export_scala_kbm (line 184) | function export_scala_kbm() {
  function export_maxmsp_coll (line 223) | function export_maxmsp_coll() {
  function export_pd_text (line 245) | function export_pd_text() {
  function export_kontakt_script (line 262) | function export_kontakt_script() {
  function export_soniccouture_nka (line 320) | function export_soniccouture_nka() {
  function exportImageLinePitchMap (line 368) | function exportImageLinePitchMap(range) {
  function exportHarmorPitchMap (line 417) | function exportHarmorPitchMap() {
  function exportSytrusPitchMap (line 421) | function exportSytrusPitchMap() {
  function getMnlgtunTuningInfoXML (line 425) | function getMnlgtunTuningInfoXML(useScaleFormat, programmer, comment) {
  function getMnlgtunFileInfoXML (line 443) | function getMnlgtunFileInfoXML(useScaleFormat, product = 'minilogue') {
  function exportMnlgtun (line 478) | function exportMnlgtun(useScaleFormat) {
  function export_reference_deflemask (line 542) | function export_reference_deflemask() {
  function exportReaperNamedNotes (line 598) | function exportReaperNamedNotes(
  function get_scale_url (line 747) | function get_scale_url() {
  function update_page_url (line 792) | function update_page_url(url = get_scale_url()) {
  function export_url (line 801) | function export_url() {

FILE: src/js/generators.js
  function generate_equal_temperament (line 5) | function generate_equal_temperament() {
  function generate_equal_temperament_data (line 30) | function generate_equal_temperament_data(divider, period) {
  function generate_rank_2_temperament (line 52) | function generate_rank_2_temperament() {
  function generate_rank_2_temperament_data (line 98) | function generate_rank_2_temperament_data(generator, period, size, up, l...
  function generate_harmonic_series_segment (line 145) | function generate_harmonic_series_segment() {
  function generate_harmonic_series_segment_data (line 174) | function generate_harmonic_series_segment_data(lo, hi) {
  function generate_subharmonic_series_segment (line 185) | function generate_subharmonic_series_segment() {
  function generate_subharmonic_series_segment_data (line 214) | function generate_subharmonic_series_segment_data(lo, hi) {
  function generate_enumerate_chord (line 224) | function generate_enumerate_chord() {
  function generate_enumerate_chord_data (line 300) | function generate_enumerate_chord_data(pitches, convertToRatios = false) {
  function generate_cps (line 329) | function generate_cps() {
  function load_preset_scale (line 422) | function load_preset_scale(a) {

FILE: src/js/graphics.js
  function render_graphic_scale_rule (line 7) | function render_graphic_scale_rule() {

FILE: src/js/helpers.js
  function mathModulo (line 14) | function mathModulo(n, d) {
  function logModulo (line 19) | function logModulo(n, d) {
  function cents_to_decimal (line 34) | function cents_to_decimal(input) {
  function ratio_to_decimal (line 41) | function ratio_to_decimal(input) {
  function commadecimal_to_decimal (line 52) | function commadecimal_to_decimal(input) {
  function decimal_to_commadecimal (line 67) | function decimal_to_commadecimal(input) {
  function decimal_to_cents (line 77) | function decimal_to_cents(input) {
  function ratio_to_cents (line 91) | function ratio_to_cents(input) {
  function n_of_edo_to_decimal (line 96) | function n_of_edo_to_decimal(input) {
  function n_of_edo_to_cents (line 107) | function n_of_edo_to_cents(input) {
  function isCent (line 111) | function isCent(input) {
  function isCommaDecimal (line 120) | function isCommaDecimal(input) {
  function isNOfEdo (line 129) | function isNOfEdo(input) {
  function isRatio (line 135) | function isRatio(input) {
  function getLineType (line 141) | function getLineType(input) {
  function line_to_decimal (line 159) | function line_to_decimal(input) {
  function line_to_commadecimal (line 181) | function line_to_commadecimal(input, padDecimals = 0, truncateDecimalsPa...
  function isNegativeInterval (line 205) | function isNegativeInterval(input) {
  function line_to_cents (line 234) | function line_to_cents(input) {
  function mtof (line 240) | function mtof(input) {
  function ftom (line 248) | function ftom(input) {
  function sanitize_filename (line 258) | function sanitize_filename(input) {
  function clear_all (line 266) | function clear_all() {
  function midi_note_number_to_name (line 298) | function midi_note_number_to_name(input) {
  function sum_array (line 307) | function sum_array(array, endIndex) {
  function rotate (line 312) | function rotate(array, steps) {
  function get_cf (line 318) | function get_cf(num, maxiterations = 15, roundf = 10) {
  function get_convergent (line 349) | function get_convergent(cf, depth = 0) {
  function decimal_to_ratio (line 391) | function decimal_to_ratio(input, iterations = 15, depth = 0) {
  function cents_to_ratio (line 405) | function cents_to_ratio(input, iterations = 15, depth = 0) {
  function n_of_edo_to_ratio (line 409) | function n_of_edo_to_ratio(input, iterations = 15, depth = 0) {
  function get_convergents (line 414) | function get_convergents(cf, numarray, denarray, perlimit, cindOut = nul...
  function show_mos_cf (line 463) | function show_mos_cf(per, gen, ssz, threshold) {
  function get_rational_approximations (line 531) | function get_rational_approximations(
  function get_rank2_mode (line 571) | function get_rank2_mode(period, generator, size, numdown = 0) {
  function get_prime_factors (line 602) | function get_prime_factors(number) {
  function get_prime_factors_string (line 640) | function get_prime_factors_string(number) {
  function isPrime (line 654) | function isPrime(number) {
  function prevPrime (line 667) | function prevPrime(number) {
  function nextPrime (line 674) | function nextPrime(number) {
  function closestPrime (line 681) | function closestPrime(number) {
  function scrollToPrime (line 694) | function scrollToPrime(number, scrollDown) {
  function get_prime_limit (line 699) | function get_prime_limit(number) {
  function get_prime_limit_of_ratio (line 704) | function get_prime_limit_of_ratio(numerator, denominator) {
  function get_coprimes (line 709) | function get_coprimes(number) {
  function get_factors (line 730) | function get_factors(number) {
  function getGCD (line 747) | function getGCD(num1, num2) {
  function getLCM (line 768) | function getLCM(num1, num2) {
  function getLCMArray (line 775) | function getLCMArray(array) {
  function ratioIsValid (line 809) | function ratioIsValid(ratio) {
  function ratioIsSafe (line 824) | function ratioIsSafe(ratio) {
  function simplifyRatio (line 833) | function simplifyRatio(ratio) {
  function transposeRatios (line 851) | function transposeRatios(ratio, transposerRatio) {
  function powRatio (line 874) | function powRatio(ratio, power) {
  function periodReduceRatio (line 902) | function periodReduceRatio(ratio, period) {
  function transposeNOfEdos (line 952) | function transposeNOfEdos(nOfEdo, transposerNOfEdo) {
  function transposeLine (line 967) | function transposeLine(line, transposer) {
  function transposeSelf (line 1024) | function transposeSelf(line, transposeAmt) {
  function moduloLine (line 1050) | function moduloLine(line, modLine) {
  function negateLine (line 1113) | function negateLine(line) {
  function invert_chord (line 1137) | function invert_chord(chord) {
  function getFloat (line 1184) | function getFloat(id, errorMessage) {
  function getString (line 1195) | function getString(id, errorMessage) {
  function getLine (line 1206) | function getLine(id, errorMessage) {
  function setScaleName (line 1222) | function setScaleName(title) {
  function closePopup (line 1226) | function closePopup(id) {
  function setTuningData (line 1230) | function setTuningData(tuning) {
  function getCoordsFromKey (line 1236) | function getCoordsFromKey(tdOfKeyboard) {
  function getSearchParamOr (line 1244) | function getSearchParamOr(valueIfMissing, key, url) {
  function getSearchParamAsNumberOr (line 1248) | function getSearchParamAsNumberOr(valueIfMissingOrNan, key, url) {
  function trimSelf (line 1254) | function trimSelf(el) {
  function openDialog (line 1260) | function openDialog(el, onOK) {
  function redirectToHTTPS (line 1274) | function redirectToHTTPS() {
  function centsTableToMnlgBinary (line 1281) | function centsTableToMnlgBinary(centsTableIn) {
  function mnlgBinaryToCents (line 1306) | function mnlgBinaryToCents(binaryData) {
  function cps_combinations (line 1321) | function cps_combinations(factors, data, start, end, index, cc, products) {
  function cps (line 1342) | function cps(factors, cc) {
  function scaleSort (line 1353) | function scaleSort(scale = []) {

FILE: src/js/keymap.js
  function buildKeymapFromLayout (line 90) | function buildKeymapFromLayout(rows) {

FILE: src/js/midi/midi.js
  class MIDI (line 13) | class MIDI extends EventEmitter {
    method constructor (line 14) | constructor() {
    method whiteOnly (line 28) | set whiteOnly(value) {
    method init (line 38) | async init() {
    method toggleDevice (line 189) | toggleDevice(type, deviceId, newValue = null) {
    method setDevice (line 210) | setDevice(type, deviceId, newValue) {
    method toggleChannel (line 214) | toggleChannel(type, deviceId, channelId, newValue = null) {
    method setChannel (line 227) | setChannel(type, deviceId, channelId, newValue) {
    method getEnabledOutputs (line 231) | getEnabledOutputs() {
    method getLowestEnabledChannel (line 237) | getLowestEnabledChannel(channels) {
    method playFrequency (line 241) | playFrequency(frequency = 0) {
    method stopFrequency (line 279) | stopFrequency() {
    method isSupported (line 283) | isSupported() {

FILE: src/js/modifiers.js
  function modify_stretch (line 6) | function modify_stretch() {
  function modify_random_variance (line 50) | function modify_random_variance() {
  function modify_mode (line 100) | function modify_mode() {
  function modify_sync_beating (line 205) | function modify_sync_beating() {
  function modify_rotate (line 258) | function modify_rotate() {
  function modify_replace_with_approximation (line 299) | function modify_replace_with_approximation() {
  function modify_update_approximations (line 338) | function modify_update_approximations() {
  function modify_approximate_harmonics (line 427) | function modify_approximate_harmonics() {
  function modify_approximate_subharmonics (line 466) | function modify_approximate_subharmonics() {
  function modify_equalize (line 505) | function modify_equalize() {
  function modify_octave_reduce (line 551) | function modify_octave_reduce() {

FILE: src/js/scaleworkshop.js
  constant APP_TITLE (line 23) | const APP_TITLE = 'Scale Workshop 1.5'
  constant TUNING_MAX_SIZE (line 24) | const TUNING_MAX_SIZE = 128
  function generate_tuning_table (line 70) | function generate_tuning_table(tuning) {
  function set_key_colors (line 90) | function set_key_colors(list) {
  function parse_url (line 115) | function parse_url() {
  function parse_tuning_data (line 203) | function parse_tuning_data() {
  function is_file_api_supported (line 329) | function is_file_api_supported() {
  function import_scala_scl (line 342) | function import_scala_scl() {
  function import_anamark_tun (line 350) | function import_anamark_tun() {
  function importMnlgtun (line 358) | function importMnlgtun() {
  function parse_imported_scala_scl (line 367) | function parse_imported_scala_scl(event) {
  function parse_imported_anamark_tun (line 403) | function parse_imported_anamark_tun(event) {
  function parseImportedMnlgtun (line 559) | function parseImportedMnlgtun(event) {

FILE: src/js/state/state.js
  class State (line 1) | class State extends EventEmitter {
    method constructor (line 2) | constructor(initialData = {}) {
    method get (line 6) | get(key) {
    method set (line 9) | set(key, newValue, forceEmit = false) {
    method ready (line 20) | ready() {

FILE: src/js/synth.js
  function keycode_to_midinote (line 16) | function keycode_to_midinote(keycode) {
  function touch_to_midinote (line 33) | function touch_to_midinote([row, col]) {
  function is_qwerty_active (line 49) | function is_qwerty_active() {
  constant LEFT_MOUSE_BTN (line 126) | const LEFT_MOUSE_BTN = 0

FILE: src/js/synth/Delay.js
  class Delay (line 1) | class Delay {
    method constructor (line 2) | constructor(synth) {
    method enable (line 8) | enable() {
    method disable (line 14) | disable() {
    method init (line 20) | init(audioCtx) {

FILE: src/js/synth/Synth.js
  class Synth (line 1) | class Synth {
    method constructor (line 2) | constructor() {
    method init (line 22) | init() {
    method setMainVolume (line 107) | setMainVolume(newValue) {
    method noteOn (line 119) | noteOn(midinote, velocity = 127) {
    method noteOff (line 153) | noteOff(midinote) {
    method now (line 172) | now() {
    method panic (line 177) | panic() {

FILE: src/js/synth/Voice.js
  function getLinearRampToValueAtTime (line 48) | function getLinearRampToValueAtTime(t, v0, v1, t0, t1) {
  function getExponentialRampToValueAtTime (line 64) | function getExponentialRampToValueAtTime(t, v0, v1, t0, t1) {
  class Voice (line 107) | class Voice {
    method constructor (line 108) | constructor(audioCtx) {
    method init (line 116) | init() {
    method start (line 129) | start(frequency, velocity) {
    method stop (line 195) | stop() {
    method cancelEnvelope (line 205) | cancelEnvelope(property, now) {
    method bindSynth (line 223) | bindSynth(synth) {
    method bindDelay (line 226) | bindDelay(delay) {

FILE: src/js/ui.js
  function touch_kbd_open (line 22) | function touch_kbd_open() {
  function touch_kbd_close (line 59) | function touch_kbd_close() {

FILE: src/js/user.js
  function run_user_scripts_on_document_ready (line 9) | function run_user_scripts_on_document_ready() {}

FILE: src/lib/bootstrap-3.3.7-dist/js/bootstrap.js
  function transitionEnd (line 34) | function transitionEnd() {
  function removeElement (line 126) | function removeElement() {
  function Plugin (line 142) | function Plugin(option) {
  function Plugin (line 251) | function Plugin(option) {
  function Plugin (line 475) | function Plugin(option) {
  function getTargetFromTrigger (line 695) | function getTargetFromTrigger($trigger) {
  function Plugin (line 707) | function Plugin(option) {
  function getParent (line 774) | function getParent($this) {
  function clearMenus (line 787) | function clearMenus(e) {
  function Plugin (line 880) | function Plugin(option) {
  function Plugin (line 1208) | function Plugin(option, _relatedTarget) {
  function complete (line 1574) | function complete() {
  function Plugin (line 1750) | function Plugin(option) {
  function Plugin (line 1859) | function Plugin(option) {
  function ScrollSpy (line 1902) | function ScrollSpy(element, options) {
  function Plugin (line 2022) | function Plugin(option) {
  function next (line 2131) | function next() {
  function Plugin (line 2177) | function Plugin(option) {
  function Plugin (line 2334) | function Plugin(option) {

FILE: src/lib/decimal.js
  function digitsToString (line 2520) | function digitsToString(d) {
  function checkInt32 (line 2550) | function checkInt32(i, min, max) {
  function checkRoundingDigits (line 2562) | function checkRoundingDigits(d, i, rm, repeating) {
  function convertBase (line 2613) | function convertBase(str, baseIn, baseOut) {
  function cosine (line 2641) | function cosine(Ctor, x) {
  function multiplyInteger (line 2681) | function multiplyInteger(x, k, base) {
  function compare (line 2697) | function compare(a, b, aL, bL) {
  function subtract (line 2714) | function subtract(a, b, aL, base) {
  function finalise (line 2946) | function finalise(x, sd, rm, isTruncated) {
  function finiteToString (line 3113) | function finiteToString(x, isExp, sd) {
  function getBase10Exponent (line 3147) | function getBase10Exponent(digits, e) {
  function getLn10 (line 3156) | function getLn10(Ctor, sd, pr) {
  function getPi (line 3168) | function getPi(Ctor, sd, rm) {
  function getPrecision (line 3174) | function getPrecision(digits) {
  function getZeroString (line 3194) | function getZeroString(k) {
  function intPow (line 3208) | function intPow(Ctor, x, n, pr) {
  function isOdd (line 3243) | function isOdd(n) {
  function maxOrMin (line 3251) | function maxOrMin(Ctor, args, ltgt) {
  function naturalExponential (line 3301) | function naturalExponential(x, sd) {
  function naturalLogarithm (line 3392) | function naturalLogarithm(y, sd) {
  function nonFiniteToString (line 3508) | function nonFiniteToString(x) {
  function parseDecimal (line 3517) | function parseDecimal(x, str) {
  function parseOther (line 3599) | function parseOther(x, str) {
  function sine (line 3679) | function sine(Ctor, x) {
  function taylorSeries (line 3713) | function taylorSeries(Ctor, n, x, y, isHyperbolic) {
  function tinyPow (line 3749) | function tinyPow(b, e) {
  function toLessThanHalfPi (line 3757) | function toLessThanHalfPi(Ctor, x) {
  function toStringBinary (line 3795) | function toStringBinary(x, baseOut, sd, rm) {
  function truncate (line 3928) | function truncate(arr, len) {
  function abs (line 3990) | function abs(x) {
  function acos (line 4001) | function acos(x) {
  function acosh (line 4013) | function acosh(x) {
  function add (line 4026) | function add(x, y) {
  function asin (line 4038) | function asin(x) {
  function asinh (line 4050) | function asinh(x) {
  function atan (line 4062) | function atan(x) {
  function atanh (line 4074) | function atanh(x) {
  function atan2 (line 4104) | function atan2(y, x) {
  function cbrt (line 4155) | function cbrt(x) {
  function ceil (line 4166) | function ceil(x) {
  function clamp (line 4179) | function clamp(x, min, max) {
  function config (line 4202) | function config(obj) {
  function cos (line 4253) | function cos(x) {
  function cosh (line 4265) | function cosh(x) {
  function clone (line 4275) | function clone(obj) {
  function div (line 4467) | function div(x, y) {
  function exp (line 4479) | function exp(x) {
  function floor (line 4490) | function floor(x) {
  function hypot (line 4504) | function hypot() {
  function isDecimalInstance (line 4534) | function isDecimalInstance(obj) {
  function ln (line 4546) | function ln(x) {
  function log (line 4561) | function log(x, y) {
  function log2 (line 4573) | function log2(x) {
  function log10 (line 4585) | function log10(x) {
  function max (line 4596) | function max() {
  function min (line 4607) | function min() {
  function mod (line 4620) | function mod(x, y) {
  function mul (line 4633) | function mul(x, y) {
  function pow (line 4646) | function pow(x, y) {
  function random (line 4659) | function random(sd) {
  function round (line 4764) | function round(x) {
  function sign (line 4780) | function sign(x) {
  function sin (line 4793) | function sin(x) {
  function sinh (line 4805) | function sinh(x) {
  function sqrt (line 4817) | function sqrt(x) {
  function sub (line 4830) | function sub(x, y) {
  function sum (line 4844) | function sum() {
  function tan (line 4864) | function tan(x) {
  function tanh (line 4876) | function tanh(x) {
  function trunc (line 4887) | function trunc(x) {

FILE: src/lib/eventemitter3.js
  function Events (line 15) | function Events() {}
  function EE (line 43) | function EE(fn, context, once) {
  function addListener (line 60) | function addListener(emitter, event, fn, context, once) {
  function clearEvent (line 82) | function clearEvent(emitter, evt) {
  function EventEmitter (line 94) | function EventEmitter() {

FILE: src/lib/jquery-ui-1.12.1/external/jquery/jquery.js
  function isArrayLike (line 563) | function isArrayLike( obj ) {
  function Sizzle (line 772) | function Sizzle( selector, context, results, seed ) {
  function createCache (line 912) | function createCache() {
  function markFunction (line 930) | function markFunction( fn ) {
  function assert (line 939) | function assert( fn ) {
  function addHandle (line 961) | function addHandle( attrs, handler ) {
  function siblingCheck (line 976) | function siblingCheck( a, b ) {
  function createInputPseudo (line 1003) | function createInputPseudo( type ) {
  function createButtonPseudo (line 1014) | function createButtonPseudo( type ) {
  function createPositionalPseudo (line 1025) | function createPositionalPseudo( fn ) {
  function testContext (line 1048) | function testContext( context ) {
  function setFilters (line 2093) | function setFilters() {}
  function toSelector (line 2164) | function toSelector( tokens ) {
  function addCombinator (line 2174) | function addCombinator( matcher, combinator, base ) {
  function elementMatcher (line 2232) | function elementMatcher( matchers ) {
  function multipleContexts (line 2246) | function multipleContexts( selector, contexts, results ) {
  function condense (line 2255) | function condense( unmatched, map, filter, context, xml ) {
  function setMatcher (line 2276) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
  function matcherFromTokens (line 2369) | function matcherFromTokens( tokens ) {
  function matcherFromGroupMatchers (line 2427) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
  function winnow (line 2765) | function winnow( elements, qualifier, not ) {
  function sibling (line 3078) | function sibling( cur, dir ) {
  function createOptions (line 3159) | function createOptions( options ) {
  function detach (line 3595) | function detach() {
  function completed (line 3609) | function completed() {
  function dataAttr (line 3779) | function dataAttr( elem, key, data ) {
  function isEmptyDataObject (line 3813) | function isEmptyDataObject( obj ) {
  function internalData (line 3829) | function internalData( elem, name, data, pvt /* Internal Use Only */ ) {
  function internalRemoveData (line 3921) | function internalRemoveData( elem, name, pvt ) {
  function adjustCSS (line 4314) | function adjustCSS( elem, prop, valueParts, tween ) {
  function createSafeFragment (line 4444) | function createSafeFragment( document ) {
  function getAll (line 4548) | function getAll( context, tag ) {
  function setGlobalEval (line 4577) | function setGlobalEval( elems, refElements ) {
  function fixDefaultChecked (line 4593) | function fixDefaultChecked( elem ) {
  function buildFragment (line 4599) | function buildFragment( elems, context, scripts, selection, ignored ) {
  function returnTrue (line 4759) | function returnTrue() {
  function returnFalse (line 4763) | function returnFalse() {
  function safeActiveElement (line 4769) | function safeActiveElement() {
  function on (line 4775) | function on( elem, types, selector, data, fn, one ) {
  function manipulationTarget (line 5890) | function manipulationTarget( elem, content ) {
  function disableScript (line 5900) | function disableScript( elem ) {
  function restoreScript (line 5904) | function restoreScript( elem ) {
  function cloneCopyEvent (line 5914) | function cloneCopyEvent( src, dest ) {
  function fixCloneNodeIssues (line 5941) | function fixCloneNodeIssues( src, dest ) {
  function domManip (line 6009) | function domManip( collection, args, callback, ignored ) {
  function remove (line 6106) | function remove( elem, selector, keepData ) {
  function actualDisplay (line 6442) | function actualDisplay( name, doc ) {
  function defaultDisplay (line 6458) | function defaultDisplay( nodeName ) {
  function computeStyleTests (line 6607) | function computeStyleTests() {
  function addGetHookIf (line 6819) | function addGetHookIf( conditionFn, hookFn ) {
  function vendorPropName (line 6862) | function vendorPropName( name ) {
  function showHide (line 6881) | function showHide( elements, show ) {
  function setPositiveNumber (line 6938) | function setPositiveNumber( elem, value, subtract ) {
  function augmentWidthOrHeight (line 6947) | function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
  function getWidthOrHeight (line 6991) | function getWidthOrHeight( elem, name, extra ) {
  function Tween (line 7374) | function Tween( elem, options, prop, end, easing ) {
  function createFxNow (line 7498) | function createFxNow() {
  function genFx (line 7506) | function genFx( type, includeWidth ) {
  function createTween (line 7526) | function createTween( value, prop, animation ) {
  function defaultPrefilter (line 7540) | function defaultPrefilter( elem, props, opts ) {
  function propFilter (line 7685) | function propFilter( props, specialEasing ) {
  function Animation (line 7722) | function Animation( elem, properties, options ) {
  function getClass (line 8803) | function getClass( elem ) {
  function addToPrefiltersOrTransports (line 9115) | function addToPrefiltersOrTransports( structure ) {
  function inspectPrefiltersOrTransports (line 9149) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
  function ajaxExtend (line 9178) | function ajaxExtend( target, src ) {
  function ajaxHandleResponses (line 9198) | function ajaxHandleResponses( s, jqXHR, responses ) {
  function ajaxConvert (line 9255) | function ajaxConvert( s, response, jqXHR, isSuccess ) {
  function done (line 9753) | function done( status, nativeStatusText, responses, headers ) {
  function getDisplay (line 9985) | function getDisplay( elem ) {
  function filterHidden (line 9989) | function filterHidden( elem ) {
  function buildParams (line 10027) | function buildParams( prefix, obj, traditional, add ) {
  function createStandardXHR (line 10346) | function createStandardXHR() {
  function createActiveXHR (line 10352) | function createActiveXHR() {
  function getWindow (line 10682) | function getWindow( elem ) {

FILE: src/lib/jquery-ui-1.12.1/jquery-ui.js
  function _super (line 128) | function _super() {
  function _superApply (line 132) | function _superApply( args ) {
  function processClassString (line 512) | function processClassString( classes, checkOption ) {
  function handlerProxy (line 595) | function handlerProxy() {
  function handlerProxy (line 639) | function handlerProxy() {
  function getOffsets (line 775) | function getOffsets( offsets, width, height ) {
  function parseCss (line 782) | function parseCss( element, property ) {
  function getDimensions (line 786) | function getDimensions( elem ) {
  function clamp (line 1482) | function clamp( value, prop, allowEmpty ) {
  function stringParse (line 1509) | function stringParse( string ) {
  function hue2rgb (line 1763) | function hue2rgb( p, q, h ) {
  function getElementStyles (line 2037) | function getElementStyles( elem ) {
  function styleDifference (line 2065) | function styleDifference( oldStyle, newStyle ) {
  function _normalizeArguments (line 2558) | function _normalizeArguments( effect, options, speed, callback ) {
  function standardAnimationOption (line 2610) | function standardAnimationOption( option ) {
  function run (line 2687) | function run( next ) {
  function parseClip (line 2835) | function parseClip( str, element ) {
  function childComplete (line 3219) | function childComplete() {
  function animComplete (line 3269) | function animComplete() {
  function visible (line 3929) | function visible( element ) {
  function reduce (line 4057) | function reduce( elem, size, border, margin ) {
  function datepicker_getZindex (line 7189) | function datepicker_getZindex( elem ) {
  function Datepicker (line 7218) | function Datepicker() {
  function datepicker_bindHover (line 9184) | function datepicker_bindHover( dpDiv ) {
  function datepicker_handleMouseover (line 9198) | function datepicker_handleMouseover() {
  function datepicker_extendRemove (line 9212) | function datepicker_extendRemove( target, props ) {
  function checkFocus (line 12247) | function checkFocus() {
  function filteredUi (line 12454) | function filteredUi( ui ) {
  function filteredUi (line 12502) | function filteredUi( ui ) {
  function isOverAxis (line 13099) | function isOverAxis( x, reference, size ) {
  function addItems (line 15923) | function addItems() {
  function delayEvent (line 16657) | function delayEvent( type, instance, container ) {
  function spinnerModifer (line 16757) | function spinnerModifer( fn ) {
  function checkFocus (line 16888) | function checkFocus() {
  function constrain (line 17539) | function constrain() {
  function complete (line 17926) | function complete() {
  function show (line 17931) | function show() {
  function position (line 18483) | function position( event ) {

FILE: src/lib/socicon/demo-files/demo.js
  function updateTest (line 17) | function updateTest() {
  function updateSize (line 23) | function updateSize() {
Condensed preview — 62 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,473K chars).
[
  {
    "path": ".editorconfig",
    "chars": 151,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*.{js}]\nindent_style = space\nindent_size = 2\nquote_type "
  },
  {
    "path": ".gitignore",
    "chars": 9,
    "preview": ".vscode\r\n"
  },
  {
    "path": ".prettierrc",
    "chars": 91,
    "preview": "{\n  \"semi\": false,\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"none\"\n}\n"
  },
  {
    "path": "README.md",
    "chars": 16904,
    "preview": "# Scale Workshop\n\n![Scale Workshop screenshot](https://raw.githubusercontent.com/SeanArchibald/scale-workshop/master/src"
  },
  {
    "path": "dev/docs/stack.md",
    "chars": 328,
    "preview": "# Stack\n\n## Client\n\n- Bootstrap 3.3.7 - https://getbootstrap.com/docs/3.3/\n- jQuery 3.2.1 - https://api.jquery.com/\n- jQ"
  },
  {
    "path": "dev/test/helpers.spec.js",
    "chars": 28184,
    "preview": "/* global describe, it, expect */\n\ndescribe(\"helpers.js\", () => {\n  describe(\"roundToNDecimals\", () => {\n    it(\"takes 2"
  },
  {
    "path": "guide.htm",
    "chars": 31642,
    "preview": "<!--\n  ____            _\n / ___|  ___ __ _| | ___\n \\___ \\ / __/ _` | |/ _ \\\n  ___) | (_| (_| | |  __/\n |____/ \\___\\__,_|"
  },
  {
    "path": "index.htm",
    "chars": 53207,
    "preview": "<!--\n  ____            _\n / ___|  ___ __ _| | ___\n \\___ \\ / __/ _` | |/ _ \\\n  ___) | (_| (_| | |  __/\n |____/ \\___\\__,_"
  },
  {
    "path": "src/assets/favicon/browserconfig.xml",
    "chars": 246,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo"
  },
  {
    "path": "src/assets/favicon/manifest.json",
    "chars": 403,
    "preview": "{\n    \"name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x1"
  },
  {
    "path": "src/css/style-dark.css",
    "chars": 2672,
    "preview": "/* \"Night Mode\" dark theme styles */\n\nbody.dark {\n  background-color: #000;\n  color: #bbb;\n}\n\n.dark p, .dark label {\n  c"
  },
  {
    "path": "src/css/style.css",
    "chars": 6893,
    "preview": "img,\ncanvas {\n  max-width: 100%;\n}\n\n.helpicon {\n  color: #999;\n  font-size: 0.9em;\n}\n\n.hidden {\n  display: none;\n}\n\n.ui-"
  },
  {
    "path": "src/js/constants.js",
    "chars": 6379,
    "preview": "const LINE_TYPE = {\n  CENTS: 'cents',\n  DECIMAL: 'decimal',\n  RATIO: 'ratio',\n  N_OF_EDO: 'n of edo',\n  INVALID: 'invali"
  },
  {
    "path": "src/js/events.js",
    "chars": 27622,
    "preview": "/*\n * EVENT HANDLERS AND OTHER DOCUMENT READY STUFF\n */\n\njQuery(document).ready(function () {\n  // automatically load ge"
  },
  {
    "path": "src/js/exporters.js",
    "chars": 26443,
    "preview": "function export_error() {\n  // no tuning data to export\n  if (R.isNil(tuning_table['freq'][tuning_table['base_midi_note'"
  },
  {
    "path": "src/js/generators.js",
    "chars": 21350,
    "preview": "/**\n * TUNING DATA GENERATORS\n */\n\nfunction generate_equal_temperament() {\n\n  var divider = getFloat('#input_number_of_d"
  },
  {
    "path": "src/js/graphics.js",
    "chars": 1098,
    "preview": "/**\n * graphics.js\n * Functions for rendering of tuning graphics\n */\n\n// draws a graphic of the scale represented as not"
  },
  {
    "path": "src/js/helpers.js",
    "chars": 37898,
    "preview": "/**\n * HELPER FUNCTIONS\n */\n\n// Set precision to cover possible integers before scientific notation\nDecimal.precision = "
  },
  {
    "path": "src/js/keymap.js",
    "chars": 1797,
    "preview": "/**\n * keymap.js\n * International keyboard layouts\n */\n\n// prettier-ignore\nvar Layouts = {\n  // English QWERTY Layout\n  "
  },
  {
    "path": "src/js/midi/commands.js",
    "chars": 1164,
    "preview": "const setPitchBendLimit = (channel, semitones) => {\n  return [\n    (commands.cc << 4) | (channel - 1),\n    cc.registered"
  },
  {
    "path": "src/js/midi/constants.js",
    "chars": 3771,
    "preview": "const whiteOnlyMap = {\r\n  0: 25,\r\n  2: 26,\r\n  4: 27,\r\n  5: 28,\r\n  7: 29,\r\n  9: 30,\r\n  11: 31,\r\n  12: 32,\r\n  14: 33,\r\n  1"
  },
  {
    "path": "src/js/midi/math.js",
    "chars": 1253,
    "preview": "const moveNUnits = (ratioOfSymmetry, divisionsPerRatio, n, frequency) => {\n  // return frequency * ratioOfSymmetry ** (n"
  },
  {
    "path": "src/js/midi/midi.js",
    "chars": 9045,
    "preview": "/**\n * midi.js\n * Capture MIDI input for synth\n */\n\nconst deviceChannelInfo = {}\n\nconst getNameFromPort = (port) => {\n  "
  },
  {
    "path": "src/js/midi/ui.js",
    "chars": 2248,
    "preview": "const MidiChannel = ({ type, deviceId, channelId, enabled }) => {\n  const template = document.createElement('template')\n"
  },
  {
    "path": "src/js/modifiers.js",
    "chars": 18132,
    "preview": "/**\n * TUNING DATA MODIFIERS\n */\n\n// stretch/compress tuning\nfunction modify_stretch() {\n  // remove white space from tu"
  },
  {
    "path": "src/js/scaleworkshop.js",
    "chars": 20366,
    "preview": "/**\n * INIT\n */\n\n// check if coming from a Back/Forward history navigation.\n// need to reload the page so that url param"
  },
  {
    "path": "src/js/state/actions-dom.js",
    "chars": 741,
    "preview": "// DOM changes, need to sync with state\n\njQuery('#input_range_main_vol').on('input', function () {\n  state.set('main vol"
  },
  {
    "path": "src/js/state/actions.js",
    "chars": 183,
    "preview": "// non-DOM changes, need to sync with state\n\ndocument.addEventListener('keydown', (event) => {\n  if (event.key === 'Esca"
  },
  {
    "path": "src/js/state/on-ready.js",
    "chars": 81,
    "preview": "// when all event hooks set up the initial change events can fire\n\nstate.ready()\n"
  },
  {
    "path": "src/js/state/reactions-dom.js",
    "chars": 1248,
    "preview": "// data changed, sync it with the DOM\n\nstate.on('main volume', (value) => {\n  jQuery('#input_range_main_vol').val(value)"
  },
  {
    "path": "src/js/state/reactions.js",
    "chars": 139,
    "preview": "// data changed, handle programmatic reaction - no DOM changes\n\nstate.on('main volume', (newValue) => {\n  synth.setMainV"
  },
  {
    "path": "src/js/state/state.js",
    "chars": 757,
    "preview": "class State extends EventEmitter {\n  constructor(initialData = {}) {\n    super()\n    this.data = initialData\n  }\n  get(k"
  },
  {
    "path": "src/js/synth/Delay.js",
    "chars": 2767,
    "preview": "class Delay {\n  constructor(synth) {\n    this.time = 0.3\n    this.gain = 0.4\n    this.inited = false\n    this.synth = sy"
  },
  {
    "path": "src/js/synth/Synth.js",
    "chars": 6740,
    "preview": "class Synth {\n  constructor() {\n    this.keymap = Keymap.EN\n    this.isomorphicMapping = {\n      vertical: 5, // how man"
  },
  {
    "path": "src/js/synth/Voice.js",
    "chars": 5595,
    "preview": "const getEnvelopeByName = (name) => {\n  const envelope = {\n    attackTime: 0,\n    decayTime: 0,\n    sustain: 1,\n    rele"
  },
  {
    "path": "src/js/synth.js",
    "chars": 4480,
    "preview": "/**\n * synth.js\n * Web audio synth\n */\n\nconst synth = new Synth()\n\n// keycode_to_midinote()\n// it turns a keycode to a M"
  },
  {
    "path": "src/js/ui.js",
    "chars": 1909,
    "preview": "/**\n * ui.js\n * User interface\n */\n\n// use jQuery UI tooltips instead of default browser tooltips\njQuery(function () {\n "
  },
  {
    "path": "src/js/user.js",
    "chars": 242,
    "preview": "/**\n * user.js\n * Add your own hacks and hotfixes below\n */\n\n// declare custom variables and functions here\n\n// any code"
  },
  {
    "path": "src/lib/bootstrap-3.3.7-dist/css/bootstrap-theme.css",
    "chars": 26132,
    "preview": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "src/lib/bootstrap-3.3.7-dist/css/bootstrap.css",
    "chars": 146010,
    "preview": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "src/lib/bootstrap-3.3.7-dist/js/bootstrap.js",
    "chars": 69707,
    "preview": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under the MIT license"
  },
  {
    "path": "src/lib/bootstrap-3.3.7-dist/js/npm.js",
    "chars": 484,
    "preview": "// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.\nrequ"
  },
  {
    "path": "src/lib/decimal.js",
    "chars": 130922,
    "preview": ";(function (globalScope) {\n  'use strict';\n\n\n  /*\n   *  decimal.js v10.3.1\n   *  An arbitrary-precision Decimal type for"
  },
  {
    "path": "src/lib/eventemitter3.js",
    "chars": 9214,
    "preview": "// source: https://github.com/primus/eventemitter3/blob/master/index.js\n\n'use strict';\n\nvar has = Object.prototype.hasOw"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/AUTHORS.txt",
    "chars": 12634,
    "preview": "Authors ordered by first contribution\nA list of current team members is available at http://jqueryui.com/about\n\nPaul Bak"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/LICENSE.txt",
    "chars": 1817,
    "preview": "Copyright jQuery Foundation and other contributors, https://jquery.org/\n\nThis software consists of voluntary contributio"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/external/jquery/jquery.js",
    "chars": 293430,
    "preview": "/*!\n * jQuery JavaScript Library v1.12.4\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Co"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/index.html",
    "chars": 32588,
    "preview": "<!doctype html>\n<html lang=\"us\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>jQuery UI Example Page</title>\n\t<link href=\"jque"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/jquery-ui.css",
    "chars": 37326,
    "preview": "/*! jQuery UI - v1.12.1 - 2016-09-14\n* http://jqueryui.com\n* Includes: core.css, accordion.css, autocomplete.css, menu.c"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/jquery-ui.js",
    "chars": 520714,
    "preview": "/*! jQuery UI - v1.12.1 - 2016-09-14\n* http://jqueryui.com\n* Includes: widget.js, position.js, data.js, disable-selectio"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/jquery-ui.structure.css",
    "chars": 18705,
    "preview": "/*!\n * jQuery UI CSS Framework 1.12.1\n * http://jqueryui.com\n *\n * Copyright jQuery Foundation and other contributors\n *"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/jquery-ui.theme.css",
    "chars": 18671,
    "preview": "/*!\n * jQuery UI CSS Framework 1.12.1\n * http://jqueryui.com\n *\n * Copyright jQuery Foundation and other contributors\n *"
  },
  {
    "path": "src/lib/jquery-ui-1.12.1/package.json",
    "chars": 1845,
    "preview": "{\n\t\"name\": \"jquery-ui\",\n\t\"title\": \"jQuery UI\",\n\t\"description\": \"A curated set of user interface interactions, effects, w"
  },
  {
    "path": "src/lib/socicon/Read Me.txt",
    "chars": 746,
    "preview": "Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.\n\nTo use the generated fo"
  },
  {
    "path": "src/lib/socicon/demo-files/demo.css",
    "chars": 1996,
    "preview": "body {\n  padding: 0;\n  margin: 0;\n  font-family: sans-serif;\n  font-size: 1em;\n  line-height: 1.5;\n  color: #555;\n  back"
  },
  {
    "path": "src/lib/socicon/demo-files/demo.js",
    "chars": 996,
    "preview": "if (!('boxShadow' in document.body.style)) {\n    document.body.setAttribute('class', 'noBoxShadow');\n}\n\ndocument.body.ad"
  },
  {
    "path": "src/lib/socicon/demo.html",
    "chars": 199055,
    "preview": "<!doctype html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>IcoMoon Demo</title>\n    <meta name=\"description\" co"
  },
  {
    "path": "src/lib/socicon/selection.json",
    "chars": 381532,
    "preview": "{\n  \"IcoMoonType\": \"selection\",\n  \"icons\": [\n    {\n      \"icon\": {\n        \"paths\": [\n          \"M512 0c-282.767 0-512 2"
  },
  {
    "path": "src/lib/socicon/style.css",
    "chars": 13290,
    "preview": "@font-face {\n  font-family: 'Socicon';\n  src:  url('fonts/Socicon.eot?484r1f');\n  src:  url('fonts/Socicon.eot?484r1f#ie"
  },
  {
    "path": "src/lib/socicon/style.less",
    "chars": 19146,
    "preview": "@import \"variables\";\n\n@font-face {\n  font-family: 'Socicon';\n  src:  url('@{icomoon-font-path}/Socicon.eot?484r1f');\n  s"
  },
  {
    "path": "src/lib/socicon/variables.less",
    "chars": 7064,
    "preview": "@icomoon-font-path: \"fonts\";\n\n@socicon-internet: \"\\e957\";\n@socicon-moddb: \"\\e94b\";\n@socicon-indiedb: \"\\e94c\";\n@socicon-t"
  },
  {
    "path": "test.html",
    "chars": 946,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Tests for Scale Workshop</title>\n    <meta charset=\"utf-8\" />\n    <link rel=\""
  }
]

About this extraction

This page contains the full source code of the SeanArchibald/scale-workshop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 62 files (2.2 MB), approximately 575.2k tokens, and a symbol index with 404 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!