├── public ├── favicon.ico ├── manifest.json ├── index.html ├── milligram.min.css ├── normalize.css └── soundfont-player.min.js ├── src ├── components │ ├── interval │ │ ├── Interval.css │ │ ├── IntervalProps.js │ │ ├── IntervalSelector.js │ │ ├── Interval.js │ │ └── Intervals.js │ ├── note │ │ ├── Note.css │ │ ├── Notes.js │ │ ├── NoteSelector.js │ │ ├── NoteProps.js │ │ └── Note.js │ ├── pcset │ │ ├── PcSet.css │ │ ├── PcSets.js │ │ ├── SearchList.js │ │ ├── ScaleChords.js │ │ ├── Scale.js │ │ ├── ScaleModes.js │ │ ├── Chord.js │ │ ├── Related.js │ │ ├── Scales.js │ │ ├── PitchSetInfo.js │ │ ├── Chords.js │ │ └── NameList.js │ ├── key │ │ ├── Key.css │ │ ├── RelatedKeys.js │ │ ├── KeyChords.js │ │ ├── Keys.js │ │ └── Key.js │ ├── shared │ │ ├── ErrorBanner.js │ │ ├── Selector.css │ │ ├── Link.js │ │ ├── Props.js │ │ ├── Selector.js │ │ ├── Layout.js │ │ ├── Code.js │ │ ├── Collapsable.js │ │ └── API.js │ ├── viz │ │ ├── CircleSet.css │ │ ├── Piano.css │ │ ├── PianoKeyboard.css │ │ ├── CircleSet.js │ │ ├── Piano.js │ │ ├── Score.js │ │ └── PianoKeyboard.js │ ├── Tonal.js │ ├── Header.js │ └── Router.js ├── index.css ├── actions.js ├── App.test.js ├── index.js ├── store.js ├── App.css ├── player.js ├── router.js ├── App.js └── registerServiceWorker.js ├── README.md ├── .gitignore ├── PULL_REQUEST_TEMPLATE.md ├── .eslintrc.json ├── package.json ├── LICENSE └── CODE_OF_CONDUCT.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonaljs/tonal-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/interval/Interval.css: -------------------------------------------------------------------------------- 1 | .IntervalProps { 2 | margin-bottom: 3rem; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/note/Note.css: -------------------------------------------------------------------------------- 1 | .NoteSelector, 2 | .NoteProps { 3 | margin-bottom: 2rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/pcset/PcSet.css: -------------------------------------------------------------------------------- 1 | .Chords .Selector, 2 | .Scales .Selector { 3 | margin-bottom: 2rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/key/Key.css: -------------------------------------------------------------------------------- 1 | .KeyScale, 2 | .RelatedKeys, 3 | .Key > .Selector { 4 | margin-bottom: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tonal-react-app 2 | 3 | A react application to showcase the possibilities of the [tonal](https://github.com/danigb/tonal) library -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_PATH = "CHANGE_PATH"; 2 | 3 | export const changePath = url => ({ type: CHANGE_PATH, payload: url }); 4 | -------------------------------------------------------------------------------- /src/components/shared/ErrorBanner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ children }) => ( 4 |
{children}
5 | ); 6 | -------------------------------------------------------------------------------- /src/components/shared/Selector.css: -------------------------------------------------------------------------------- 1 | .Selector { 2 | width: 100%; 3 | overflow-wrap: break-word; 4 | } 5 | .Selector label { 6 | display: inline; 7 | margin-right: 1rem; 8 | } 9 | 10 | .Selector a { 11 | margin-right: 1rem; 12 | } 13 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/shared/Link.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const encode = arr => "#/" + arr.join("/").replace(/ /g, "_"); 4 | 5 | export default ({ to, alt, children }) => ( 6 | 7 | {children} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/viz/CircleSet.css: -------------------------------------------------------------------------------- 1 | .Circle .background { 2 | fill: #ede3e3; 3 | } 4 | .Circle .tonic { 5 | fill: #9b4dca; 6 | } 7 | 8 | .Circle polygon { 9 | fill: #e9d7f5; 10 | stroke: #9b4dca; 11 | } 12 | .NOCircle polygon { 13 | fill: #65c3de; 14 | stroke: #4a8ea2; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/viz/Piano.css: -------------------------------------------------------------------------------- 1 | .piano-key { 2 | stroke: #3b3636; 3 | fill: #fffff7; 4 | } 5 | .piano-key.black { 6 | fill: #4b4b4b; 7 | } 8 | .piano-key.active { 9 | fill: #e9cbcb; 10 | stroke: #a534eb; 11 | } 12 | .piano-key.black.active { 13 | stroke: #e9cbcb; 14 | fill: #a534eb; 15 | } 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/components/shared/Props.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ names, children }) => ( 4 | 5 | 6 | 7 | {names.map((name, i) => ( 8 | 11 | ))} 12 | 13 | 14 | {children} 15 |
9 | {name} 10 |
16 | ); 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { initRouter } from "./router"; 5 | import store from "./store"; 6 | import App from "./App"; 7 | 8 | initRouter(store); 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/shared/Selector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "./Link"; 3 | import "./Selector.css"; 4 | 5 | export default ({ label, items, route }) => ( 6 |
7 | {label && } 8 | {items.map((item, i) => ( 9 | 10 | {item} 11 | 12 | ))} 13 |
14 | ); 15 | -------------------------------------------------------------------------------- /src/components/Tonal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default () => ( 4 |
5 |
6 |

Tonal

7 |

8 | This is a sample React app to showcase the possibilities of  9 | tonal javascript music 10 | theory library. 11 |

12 |

Choose a category from above.

13 |
14 |
15 | ); 16 | -------------------------------------------------------------------------------- /src/components/viz/PianoKeyboard.css: -------------------------------------------------------------------------------- 1 | .piano-key { 2 | stroke: #3b3636; 3 | fill: #fff; 4 | -fill: #efefef; 5 | } 6 | .piano-key.black { 7 | stroke: #3b3636; 8 | -fill: #efefef; 9 | fill: #000; 10 | } 11 | .piano-key.active { 12 | fill: #e9d7f5; 13 | } 14 | .piano-key.black.active { 15 | fill: #e9d7f5; 16 | -fill: #a077b9; 17 | } 18 | .piano-key.white.tonic { 19 | fill: #a077b9; 20 | } 21 | .piano-key.black.tonic { 22 | fill: #a077b9; 23 | -fill: #54c5c5; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/shared/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import API from "./API"; 3 | 4 | export const Layout = ({ modules, children }) => ( 5 |
6 |
{children}
7 |
8 | 9 |
10 |
11 | ); 12 | 13 | export const withLayout = (modules, Component) => props => ( 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What this PR do? 2 | 3 | ### Types of changes 4 | 5 | [ ] Refactor (code change that does not change external functionality) 6 | [ ] Bug fix (non-breaking change which fixes an issue) 7 | [ ] New feature (non-breaking change which adds functionality) 8 | [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 9 | 10 | ### Why are we doing this? Any context or related work? 11 | 12 | ### Where should a reviewer start? 13 | 14 | ### Testing steps 15 | 16 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from "redux"; 2 | import { reducer as routeReducer } from "./router"; 3 | import { CHANGE_PATH } from "./actions"; 4 | 5 | const pathReducer = (state = "", action) => { 6 | switch (action.type) { 7 | case CHANGE_PATH: 8 | return action.payload; 9 | 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | const reducer = combineReducers({ 16 | path: pathReducer, 17 | route: routeReducer 18 | }); 19 | 20 | export default createStore(reducer); 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/shared/Code.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const json = p => JSON.stringify(p, null, 2); 4 | 5 | export const val = t => 6 | t === undefined 7 | ? "undefined" 8 | : t === null ? "null" : typeof t === "string" ? `"${t}"` : t; 9 | 10 | export const arr = (arr, max) => { 11 | if (max) arr = arr.slice(0, max); 12 | return "[" + arr.map(val).join(", ") + (max ? ", ...]" : "]"); 13 | }; 14 | 15 | export default ({ lines }) => ( 16 |
17 |     {lines.join("\n")}
18 |   
19 | ); 20 | -------------------------------------------------------------------------------- /src/components/pcset/PcSets.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PcSet, Dictionary } from "tonal"; 3 | import { withLayout } from "../shared/Layout"; 4 | 5 | export default withLayout({ pcset: PcSet }, () => ( 6 |
7 |

Pitch Class Sets

8 | 9 | 10 | {PcSet.chromas().map(chroma => ( 11 | 12 | 13 | 14 | 15 | ))} 16 | 17 |
{chroma}{Dictionary.pcset.names(chroma).join(" ") || ""}
18 |
19 | )); 20 | -------------------------------------------------------------------------------- /src/components/interval/IntervalProps.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Interval } from "tonal"; 3 | 4 | export default ({ props }) => ( 5 |
6 |
7 |
8 | 9 | {Interval.simplify(props.name)} 10 | 11 | {Interval.invert(props.name)} 12 |
13 |
14 | 15 | {props.semitones} 16 | 17 | {Interval.ic(props.name)} 18 |
19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/components/interval/IntervalSelector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Interval } from "tonal"; 3 | import Selector from "../shared/Selector"; 4 | 5 | const IVLS = Interval.names(); 6 | const OCTS = [0, 1, 2, 3, 4]; 7 | const octs = props => 8 | OCTS.map(oct => Interval.build({ step: props.step, alt: props.alt, oct })); 9 | 10 | export default ({ props }) => ( 11 |
12 |

Change current interval

13 | ["interval", i]} /> 14 | ["interval", i]} 18 | /> 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /src/components/pcset/SearchList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Downshift from "downshift"; 3 | import NameList from "./NameList"; 4 | 5 | export default ({ title, type, tonic, filter, route }) => ( 6 | 7 | {({ getInputProps, inputValue }) => ( 8 |
9 | 15 | 22 |
23 | )} 24 |
25 | ); 26 | -------------------------------------------------------------------------------- /src/components/key/RelatedKeys.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Key from "tonal-key"; 3 | import Selector from "../shared/Selector"; 4 | 5 | export default ({ keyName }) => { 6 | const tonic = Key.props(keyName).tonic; 7 | const relatives = Key.modeNames().map(name => Key.relative(name, keyName)); 8 | const paralells = Key.modeNames().map(name => tonic + " " + name); 9 | 10 | return ( 11 |
12 |

Related keys

13 | 14 | ["key", k]} /> 15 | 16 | ["key", k]} /> 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/key/KeyChords.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Key from "tonal-key"; 3 | 4 | export default ({ keyName }) => ( 5 |
6 |

Chords

7 | 8 | 9 | 10 | 11 | {Key.degrees(keyName).map(degree => )} 12 | 13 | 14 | 15 | 16 | 17 | {Key.chords(keyName).map(chord => )} 18 | 19 | 20 | 21 | {Key.secDomChords(keyName).map(chord => )} 22 | 23 | 24 |
Degrees{degree}
Chords{chord}
V7{chord}
25 |
26 | ); 27 | -------------------------------------------------------------------------------- /src/components/shared/Collapsable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default class Collapsable extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { collapsed: this.props.collapsed || false }; 7 | } 8 | render() { 9 | const toggle = e => { 10 | e.preventDefault(); 11 | this.setState({ collapsed: !this.state.collapsed }); 12 | }; 13 | return ( 14 |
15 |

16 | 17 | {this.state.collapsed ? "Show " : "Hide "} 18 | {this.props.title} 19 | 20 |

21 | {this.state.collapsed ?
: this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/interval/Interval.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Interval } from "tonal"; 3 | import IntervalProps from "./IntervalProps"; 4 | import IntervalSelector from "./IntervalSelector"; 5 | import { withLayout } from "../shared/Layout"; 6 | import Code, { json } from "../shared/Code"; 7 | import "./Interval.css"; 8 | 9 | export default withLayout({ interval: Interval }, ({ interval }) => { 10 | const props = Interval.props(interval); 11 | return ( 12 |
13 |
Interval
14 |

{interval}

15 | 16 | 17 |

Properties

18 | ${json(props)}`]} 20 | /> 21 |
22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tonalrap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://danigb.github.io/tonal-app", 6 | "dependencies": { 7 | "classwrap": "^1.1.0", 8 | "downshift": "^1.10.1", 9 | "gh-pages": "^1.0.0", 10 | "prop-types": "^15.6.0", 11 | "react": "^16.0.0", 12 | "react-dom": "^16.0.0", 13 | "react-redux": "^5.0.6", 14 | "react-scripts": "1.0.14", 15 | "redux": "^3.7.2", 16 | "tonal": "^1.1.0", 17 | "tonal-abc-notation": "^1.0.0-pre5", 18 | "tonal-key": "^1.0.0-pre5" 19 | }, 20 | "scripts": { 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d build", 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/pcset/ScaleChords.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Scale } from "tonal"; 3 | import NameList from "./NameList"; 4 | import Code, { arr } from "../shared/Code"; 5 | import Collapsable from "../shared/Collapsable"; 6 | 7 | export default ({ tonic, name }) => { 8 | const chords = Scale.chords(name); 9 | return ( 10 |
11 |

Chords

12 |

All chords that fits this scale

13 | ${arr(chords, 3)}`]} /> 14 | 15 | ["chord", name]} 20 | /> 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/pcset/Scale.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note, Scale } from "tonal"; 3 | import PitchSetInfo from "./PitchSetInfo"; 4 | import Related from "./Related"; 5 | import ScaleChords from "./ScaleChords"; 6 | import ScaleModes from "./ScaleModes"; 7 | import { withLayout } from "../shared/Layout"; 8 | 9 | export default withLayout({ scale: Scale }, ({ note, name }) => { 10 | const tonic = Note.pc(note); 11 | const tonics = Note.names(); 12 | return ( 13 |
14 |
Scale
15 |

16 | {tonic} {name} 17 |

18 | 19 | 20 | 21 | 22 |
23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/note/Notes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note } from "tonal"; 3 | import { withLayout } from "../shared/Layout"; 4 | 5 | export const TONICS = "C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B B# Cb".split( 6 | " " 7 | ); 8 | 9 | export default withLayout({ note: Note }, () => ( 10 |
11 |

Notes

12 | 13 | 14 | {TONICS.map(t => ( 15 | 16 | 21 | {[2, 3, 4, 5].map(o => ( 22 | 25 | ))} 26 | 27 | ))} 28 | 29 |
17 | 18 | {t} 19 | 20 | 23 | {t + o} 24 |
30 |
31 | )); 32 | -------------------------------------------------------------------------------- /src/components/note/NoteSelector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Selector from "../shared/Selector"; 3 | 4 | const NOTES = ["C", "D", "E", "F", "G", "A", "B"]; 5 | const ACCS = ["bbb", "bb", "b", "(none)", "#", "##", "###"]; 6 | const OCTS = [1, 2, 3, 4, 5, 6, 7]; 7 | 8 | export default ({ props }) => ( 9 |
10 |

Change current note

11 | ["note", l + props.acc + props.octStr]} 15 | /> 16 | [ 20 | "note", 21 | props.letter + (acc === "(none)" ? "" : acc) + props.octStr 22 | ]} 23 | /> 24 | ["note", props.letter + props.acc + o]} 28 | /> 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /src/components/interval/Intervals.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withLayout } from "../shared/Layout"; 3 | import { Interval, Array } from "tonal"; 4 | 5 | const NUMS = Array.range(1, 15); 6 | 7 | const Ivl = props => { 8 | const ivl = Interval.build(props); 9 | return {ivl}; 10 | }; 11 | 12 | export default withLayout({ interval: Interval }, () => ( 13 |
14 |

Intervals

15 | 16 | 17 | {NUMS.map(num => ( 18 | 19 | 22 | 27 | 30 | 31 | ))} 32 | 33 |
20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 |
34 |
35 | )); 36 | -------------------------------------------------------------------------------- /src/components/note/NoteProps.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note } from "tonal"; 3 | import { toAbc } from "tonal-abc-notation"; 4 | import player from "../../player"; 5 | 6 | export default ({ props }) => ( 7 |
8 |
9 |
10 | 11 | {Note.simplify(props.name)} 12 | 13 | {Note.enharmonic(props.name)} 14 | 15 | {toAbc(props.name)} 16 |
17 |
18 | 19 | {props.midi} 20 | 21 | {props.freq ? props.freq.toFixed(2) + "Hz" : ""} 22 |
23 |
24 | {props.midi && ( 25 |

26 |
27 | 28 |

29 | )} 30 |
31 | ); 32 | -------------------------------------------------------------------------------- /src/components/pcset/ScaleModes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Scale } from "tonal"; 3 | import Code, { arr } from "../shared/Code"; 4 | import Collapsable from "../shared/Collapsable"; 5 | import NameList from "./NameList"; 6 | 7 | export default ({ tonic, name }) => { 8 | const scaleName = tonic ? tonic + " " + name : name; 9 | const modes = Scale.modeNames(scaleName); 10 | const tonics = modes.map(m => m[0]); 11 | const names = modes.map(m => m[1]); 12 | return ( 13 |
14 |

Scale modes

15 | ${arr(modes, 3)}`]} 17 | /> 18 | 19 | ["scale", name, t ? t : ""]} 24 | /> 25 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/key/Keys.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Array } from "tonal"; 3 | import * as Key from "tonal-key"; 4 | import Link from "../shared/Link"; 5 | import { withLayout } from "../shared/Layout"; 6 | 7 | const ALTS = Array.range(-5, 5); 8 | 9 | const KeyRow = ({ keyName }) => { 10 | const minor = Key.relative("minor", keyName); 11 | return ( 12 | 13 | {Key.props(keyName).acc} 14 | 15 | {keyName} 16 | 17 | 18 | {minor} 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default withLayout({ key: Key }, () => ( 25 |
26 |

Keys

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {ALTS.map(alt => )} 37 | 38 |
AccidentalsMajorMinor
39 |
40 | )); 41 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note } from "tonal"; 3 | import PianoKeyboard from "./viz/PianoKeyboard"; 4 | import { routeToHash as to } from "../router"; 5 | 6 | const Navigation = ({ route }) => ( 7 |
8 | Tonal 9 | Notes 10 | Intervals 11 | Scales 12 | Chords 13 | Keys 14 |
15 | ); 16 | 17 | const TonicSelector = ({ current, onChange }) => { 18 | return ( 19 | 25 | ); 26 | }; 27 | export default ({ route, onTonicChange }) => ( 28 |
29 |
30 | 31 | 32 |
33 |
34 | ); 35 | -------------------------------------------------------------------------------- /src/components/note/Note.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note, PcSet } from "tonal"; 3 | import * as Abc from "tonal-abc-notation"; 4 | import NoteProps from "./NoteProps"; 5 | import NoteSelector from "./NoteSelector"; 6 | import { withLayout } from "../shared/Layout"; 7 | import Code, { json } from "../shared/Code"; 8 | import "./Note.css"; 9 | import { setRoute } from "../../router"; 10 | 11 | const MODULES = { note: Note, "abc-notation": Abc }; 12 | export default withLayout(MODULES, ({ note }) => { 13 | const props = Note.props(note); 14 | return ( 15 |
16 |
Note
17 |

{note}

18 |

Properties

19 | 20 |

Code examples

21 | ${json(props)}`, 25 | `tonal.note.simplify("${note}") // => "${Note.simplify(note)}"`, 26 | `tonal.note.enharmonic("${note}") // => "${Note.enharmonic(note)}"` 27 | ]} 28 | /> 29 |
30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 danigb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/viz/CircleSet.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./CircleSet.css"; 3 | 4 | export default ({ size = 80, offset = 0, chroma = "0", type = "set" }) => { 5 | const center = size / 2; 6 | const strokeWidth = size * 0.1; 7 | const radius = size / 2 - strokeWidth / 2; 8 | // const circumference = 2 * Math.PI * radius; 9 | const radians = 2 * Math.PI / chroma.length; 10 | const points = chroma.split("").reduce((points, value, i) => { 11 | if (value === "1") { 12 | points.push(center + radius * Math.cos((offset + i - 3) * radians)); 13 | points.push(center + radius * Math.sin((offset + i - 3) * radians)); 14 | } 15 | return points; 16 | }, []); 17 | 18 | const classNames = "Circle " + type; 19 | 20 | return ( 21 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/pcset/Chord.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note, Chord } from "tonal"; 3 | import { withLayout } from "../shared/Layout"; 4 | import PitchSetInfo from "./PitchSetInfo"; 5 | import Related from "./Related"; 6 | import Selector from "../shared/Selector"; 7 | import Code, { arr, json } from "../shared/Code"; 8 | 9 | export default withLayout({ chord: Chord }, ({ note, id }) => { 10 | const tonic = Note.pc(note); 11 | const type = id; 12 | const name = tonic + type; 13 | const tokens = Chord.tokenize(name); 14 | const props = Chord.props(type); 15 | return ( 16 |
17 | ["chord", t + type]} /> 18 |

chord

19 |

{tonic ? tonic + type : type}

20 | 21 | 22 |

Code examples

23 | ${arr(tokens)}`, 26 | `Tonal.Chord.props("${type}") // => ${json(props)}` 27 | ]} 28 | /> 29 |
30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/pcset/Related.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chord, Scale } from "tonal"; 3 | import NameList from "./NameList"; 4 | import Code, { arr } from "../shared/Code"; 5 | import Collapsable from "../shared/Collapsable"; 6 | 7 | export const Set = ({ title, type, tonic, name, fnName }) => { 8 | const Set = type === "scale" ? Scale : Chord; 9 | const names = Set[fnName](name); 10 | 11 | return ( 12 |
13 |

{title}

14 |

15 | All the {type}s that includes all the notes of '{name}' and more 16 |

17 | ${arr(names, 3)}`]} 19 | /> 20 | 21 | ["scale", name, tonic ? tonic : ""]} 26 | /> 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default props => [ 33 |

Related {props.type}s

, 34 | , 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 2 | 3 | html { 4 | background-color: #efefef; 5 | } 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, sans-serif; 8 | font-weight: normal; 9 | margin: 0 auto; 10 | max-width: 92rem; 11 | padding: 0.1rem 1rem 4rem 1em; 12 | background-color: white; 13 | box-shadow: 1px 1px rgba(0, 0, 0, 0.1); 14 | margin-bottom: 4rem; 15 | } 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | font-family: "Montserrat", sans-serif; 23 | color: black; 24 | } 25 | h1.big { 26 | font-size: 8rem; 27 | } 28 | h6 { 29 | margin: 0; 30 | } 31 | 32 | table > thead > tr { 33 | font-weight: bold; 34 | } 35 | 36 | ul > li { 37 | list-style-type: none; 38 | margin: 0; 39 | } 40 | li button.button-clear { 41 | padding: 0 0.25rem; 42 | margin: 0 0.25rem; 43 | } 44 | 45 | li > strong, 46 | li > span { 47 | margin-right: 1rem; 48 | } 49 | 50 | .App { 51 | padding: 1rem 0; 52 | } 53 | 54 | .Header { 55 | margin-bottom: 2rem; 56 | } 57 | .Header .Navigation { 58 | margin-bottom: 1rem; 59 | } 60 | .Header .Navigation a { 61 | margin-right: 1rem; 62 | } 63 | 64 | .ErrorBanner { 65 | padding: 1rem; 66 | padding-bottom: 1rem; 67 | border: thin solid #ff4444; 68 | border-radius: 3px; 69 | margin-bottom: 1rem; 70 | } 71 | -------------------------------------------------------------------------------- /src/player.js: -------------------------------------------------------------------------------- 1 | import { Note, transpose } from "tonal"; 2 | const ac = new AudioContext(); 3 | let piano = null; 4 | 5 | export const loadPiano = Soundfont => { 6 | console.log("Loading..."); 7 | return Soundfont.instrument(ac, "acoustic_grand_piano").then(inst => { 8 | console.log("Piano loaded!"); 9 | piano = inst; 10 | }); 11 | }; 12 | 13 | const centered = tonic => { 14 | const pc = Note.pc(tonic); 15 | const oct = pc[0] === "A" || pc[0] === "B" ? 3 : 4; 16 | return pc + oct; 17 | }; 18 | 19 | const buildScale = (tonic, intervals) => { 20 | const scale = intervals.map(transpose(centered(tonic))); 21 | const rev = scale.slice().reverse(); 22 | scale.push(transpose(scale[0], "P8")); 23 | return scale.concat(rev); 24 | }; 25 | 26 | const buildChord = (tonic, intervals) => { 27 | return intervals.map(transpose(centered(tonic))); 28 | }; 29 | 30 | export default (tonic, intervals, type) => { 31 | if (!piano) return; 32 | const notes = 33 | type === undefined 34 | ? [tonic] 35 | : type === "scale" 36 | ? buildScale(tonic, intervals) 37 | : buildChord(tonic, intervals); 38 | const events = notes.map((note, i) => ({ 39 | time: type === "chord" ? 0 : i * 0.5, 40 | note 41 | })); 42 | 43 | piano.stop(ac.currentTime); 44 | piano.schedule(ac.currentTime, events); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/shared/API.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const apiUrl = (mod, fn) => 4 | `http://danigb.github.io/tonal/api/module-${mod}.html#.${fn}`; 5 | 6 | const Function = ({ module, name }) => ( 7 |
8 | 9 | 10 | {module}.{name} 11 | 12 | 13 |
14 | ); 15 | 16 | const npmUrl = name => `https://www.npmjs.com/package/${name}/`; 17 | const nodeiCo = name => `https://nodei.co/npm/${name}.png?mini=true`; 18 | 19 | export const Npm = ({ packageName }) => ( 20 | 21 | {packageName 22 | 23 | ); 24 | 25 | export const Module = ({ name, module }) => ( 26 |
27 |

28 | tonal-{name} 29 |

30 | 31 |
32 | {Object.keys(module || {}) 33 | .sort() 34 | .map((fn, i) => )} 35 |
36 |
37 | ); 38 | 39 | export default ({ modules }) => ( 40 |
41 |

API

42 | {Object.keys(modules).map(name => ( 43 | 44 | ))} 45 |
46 | ); 47 | -------------------------------------------------------------------------------- /src/components/viz/Piano.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Piano.css"; 3 | 4 | const WHITES = "C D E F G A B".split(" "); 5 | const BLACKS = "Db Eb Gb Ab Bb".split(" "); 6 | const BPOS = [1, 2, 4, 5, 6]; 7 | 8 | export const Octave = ({ oct, width, height }) => { 9 | const bwidth = Math.floor(0.55 * width); 10 | const hbwidth = Math.floor(0.5 * bwidth); 11 | const bheight = Math.floor(height * 0.65); 12 | return ( 13 | 14 | {WHITES.map((note, i) => ( 15 | 22 | ))} 23 | {BLACKS.map((note, i) => ( 24 | 31 | ))} 32 | 33 | ); 34 | }; 35 | 36 | export default ({ className, width = 40, height = 150 }) => { 37 | return ( 38 |
39 | 45 | 46 | 47 | 48 | 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | // Redux Actions 2 | export const CHANGE_PATH = "CHANGE_PATH"; 3 | export const changePath = url => ({ type: CHANGE_PATH, payload: url }); 4 | 5 | // init 6 | const getRoute = hash => { 7 | hash = hash || window.location.hash; 8 | const decoded = hash 9 | .split("?")[0] 10 | .slice(2) 11 | .replace(/_/g, " ") 12 | .split("/"); 13 | const [note, path, id] = decoded; 14 | return { note, path, id }; 15 | }; 16 | const getHash = () => window.location.hash.slice(2); 17 | 18 | // the route reducer 19 | const initialState = { 20 | note: undefined, 21 | path: undefined, 22 | id: undefined 23 | }; 24 | 25 | export const reducer = (state = initialState, action) => { 26 | switch (action.type) { 27 | case CHANGE_PATH: 28 | return getRoute(action.payload); 29 | 30 | default: 31 | return state; 32 | } 33 | }; 34 | 35 | export function setHash(hash) { 36 | window.location.hash = hash; 37 | } 38 | 39 | const slash = o => (o ? "/" + o : ""); 40 | 41 | export const routeToHash = (note, path, id) => 42 | ("#" + slash(note) + slash(path) + slash(id)).replace(/ /g, "_"); 43 | 44 | export function setRoute(note, path, id) { 45 | setHash(routeToHash(note, path, id)); 46 | } 47 | 48 | export function initRouter(store) { 49 | window.onhashchange = () => { 50 | store.dispatch(changePath(window.location.hash)); 51 | }; 52 | if (getHash() === "") setHash("#/C4"); 53 | window.onhashchange(); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/pcset/Scales.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withLayout } from "../shared/Layout"; 3 | import Selector from "../shared/Selector"; 4 | import SearchList from "./SearchList"; 5 | import NameList from "./NameList"; 6 | import { Scale, Note } from "tonal"; 7 | import "./PcSet.css"; 8 | 9 | const NAMES = Scale.names().sort( 10 | (a, b) => Scale.intervals(a).length - Scale.intervals(b).length 11 | ); 12 | const COMMON = [ 13 | "major", 14 | "minor", 15 | "harmonic minor", 16 | "melodic minor", 17 | "major pentatonic", 18 | "minor pentatonic", 19 | "dorian", 20 | "whole tone" 21 | ]; 22 | 23 | const filter = term => { 24 | term = term.toLowerCase(); 25 | return term === "" 26 | ? [] 27 | : NAMES.filter(name => name.toLowerCase().includes(term)); 28 | }; 29 | 30 | const API = { scale: Scale }; 31 | 32 | export default withLayout(API, ({ note }) => { 33 | const tonic = Note.pc(note); 34 | return ( 35 |
36 |

Scales {tonic && " in " + tonic}

37 | [note, "scale", name]} 43 | /> 44 |

Common scales

45 | [note, "scale", name]} 48 | tonic={tonic} 49 | names={COMMON} 50 | /> 51 |
52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import "./App.css"; 4 | import Header from "./components/Header"; 5 | import Router from "./components/Router"; 6 | import ErrorBanner from "./components/shared/ErrorBanner"; 7 | import { setRoute } from "./router"; 8 | import { loadPiano } from "../src/player"; 9 | 10 | import { Note } from "tonal"; 11 | 12 | class App extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | loadPianoError: null, 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | if (!window.Soundfont) { 22 | setTimeout(() => this.loadPianoResource(window.Soundfont), 1000); 23 | } 24 | } 25 | 26 | loadPianoResource(soundfont) { 27 | loadPiano(soundfont) 28 | .catch(error => this.setState({ loadPianoError: error })); 29 | } 30 | 31 | render() { 32 | const { path, route } = this.props; 33 | const handleTonicChange = midi => { 34 | const tonic = Note.fromMidi(midi); 35 | setRoute(tonic, route.path, route.id); 36 | }; 37 | 38 | return ( 39 |
40 | { 41 | this.state.loadPianoError && 42 | 43 |

Oh No...

44 |

Failed to load piano player resource. Please turn off any active adblock extensions.

45 |
46 | } 47 |
48 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default connect(state => state)(App); 55 | -------------------------------------------------------------------------------- /src/components/Router.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tonal from "./Tonal"; 3 | import Notes from "./note/Notes"; 4 | import Note from "./note/Note"; 5 | import Intervals from "./interval/Intervals"; 6 | import Interval from "./interval/Interval"; 7 | import PcSets from "./pcset/PcSets"; 8 | import Scales from "./pcset/Scales"; 9 | import Scale from "./pcset/Scale"; 10 | import Chords from "./pcset/Chords"; 11 | import Chord from "./pcset/Chord"; 12 | import Keys from "./key/Keys"; 13 | import Key from "./key/Key"; 14 | 15 | const decode = str => str.replace(/_/g, " "); 16 | 17 | export default ({ route }) => { 18 | if (!route.note) return {JSON.stringify(route)}; 19 | switch (route.path) { 20 | case "scales": 21 | return ; 22 | case "scale": 23 | return ; 24 | case "chords": 25 | return ; 26 | case "chord": 27 | return ; 28 | case "key": 29 | return ; 30 | default: 31 | return ; 32 | } 33 | }; 34 | 35 | export const old = ({ path, route }) => { 36 | const [zero, one, two] = path.split("/"); 37 | switch (zero) { 38 | case "notes": 39 | return ; 40 | case "note": 41 | return ; 42 | case "intervals": 43 | return ; 44 | case "interval": 45 | return ; 46 | case "keys": 47 | return ; 48 | case "key": 49 | return ; 50 | case "pcsets": 51 | return ; 52 | default: 53 | return ; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/pcset/PitchSetInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PcSet, Scale, Chord, Note, transpose } from "tonal"; 3 | import CircleSet from "../viz/CircleSet"; 4 | import PianoKeyboard from "../viz/PianoKeyboard"; 5 | import Score from "../viz/Score"; 6 | import player from "../../player"; 7 | 8 | const center = pc => 9 | pc ? (pc[0] === "A" || pc[0] === "B" ? pc + 3 : pc + 4) : null; 10 | 11 | export default ({ tonic, name, type }) => { 12 | const Set = type === "scale" ? Scale : Chord; 13 | const intervals = Set.intervals(name); 14 | const pc = Note.pc(tonic); 15 | const pcset = intervals.map(transpose(pc)); 16 | const notes = intervals.map(transpose(center(pc))); 17 | const offset = Note.chroma(tonic) || 0; 18 | 19 | return ( 20 |
21 |
22 |
23 | 28 |
29 |
30 | 31 | {intervals.join(" ")} 32 | 33 | {tonic ? pcset.join(" ") : "no tonic"} 34 |
35 |
36 | {tonic && } 37 | {tonic && type === "chrod" ? ( 38 | 39 | ) : ( 40 | 44 | )} 45 | {tonic && ( 46 |

47 | 48 |

49 | )} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/viz/Score.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Note } from "tonal"; 3 | import PropTypes from "prop-types"; 4 | 5 | const W = 512; 6 | const H = 120; 7 | 8 | class Score extends PureComponent { 9 | componentDidMount() { 10 | this.updateCanvas(); 11 | } 12 | componentDidUpdate() { 13 | this.updateCanvas(); 14 | } 15 | 16 | updateCanvas() { 17 | if (window.Vex === undefined) { 18 | setTimeout(() => this.updateCanvas(), 500); 19 | return; 20 | } 21 | try { 22 | const Vex = window.Vex; 23 | const { Renderer, Formatter } = Vex.Flow; 24 | const renderer = new Renderer(this.refs.canvas, Renderer.Backends.CANVAS); 25 | const ctx = renderer.getContext(); 26 | ctx.clearRect(0, 0, W, H); 27 | var stave = new Vex.Flow.Stave(0, 0, W - 5); 28 | stave.addClef("treble").setContext(ctx); 29 | if (this.props.keyTonic) stave.addKeySignature(this.props.keyTonic); 30 | 31 | stave.draw(); 32 | 33 | Formatter.FormatAndDraw( 34 | ctx, 35 | stave, 36 | this.props.notes.map(function(n) { 37 | const { letter, acc, oct } = Note.props(n); 38 | 39 | const note = new Vex.Flow.StaveNote({ 40 | keys: [letter + "/" + oct], 41 | duration: "q" 42 | }); 43 | if (acc) note.addAccidental(0, new Vex.Flow.Accidental(acc)); 44 | return note; 45 | }) 46 | ); 47 | } catch (e) { 48 | console.warn("VexFlow problem", e); 49 | } 50 | } 51 | 52 | render() { 53 | return ; 54 | } 55 | } 56 | Score.propTypes = { 57 | key: PropTypes.string, 58 | notes: PropTypes.arrayOf(PropTypes.string) 59 | }; 60 | 61 | export default Score; 62 | -------------------------------------------------------------------------------- /src/components/pcset/Chords.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chord, Note } from "tonal"; 3 | import { withLayout } from "../shared/Layout"; 4 | import Selector from "../shared/Selector"; 5 | import SearchList from "./SearchList"; 6 | import NameList from "./NameList"; 7 | import { routeToHash as to } from "../../router"; 8 | import "./PcSet.css"; 9 | 10 | const NAMES = Chord.names(); 11 | 12 | const byNotes = num => 13 | Chord.names() 14 | .filter(name => Chord.intervals(name).length === num) 15 | .sort(); 16 | const TRIADS = byNotes(3); 17 | const CUATRIADS = byNotes(4); 18 | 19 | const filter = term => 20 | term === "" ? [] : NAMES.filter(name => name.includes(term)); 21 | 22 | const ChordList = ({ note, names }) => ( 23 |
24 | {names.map(name => ( 25 | 26 | {name} 27 | 28 | ))} 29 |
30 | ); 31 | 32 | const API = { chord: Chord }; 33 | export default withLayout(API, ({ note }) => { 34 | const tonic = Note.pc(note); 35 | return ( 36 |
37 |

{tonic ? tonic + " Chords" : "Chords"}

38 |

Search chords

39 | [tonic, "chord", name]} 45 | /> 46 |

Three note chords

47 | [note, "chord", name]} 50 | tonic={tonic} 51 | names={TRIADS} 52 | /> 53 |

Four note chords

54 | [note, "chord", name]} 57 | tonic={tonic} 58 | names={CUATRIADS} 59 | /> 60 |
61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/key/Key.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Note, transpose } from "tonal"; 3 | import * as Key from "tonal-key"; 4 | import Score from "../viz/Score"; 5 | import KeyChords from "./KeyChords"; 6 | import RelatedKeys from "./RelatedKeys"; 7 | import Selector from "../shared/Selector"; 8 | import Code, { json } from "../shared/Code"; 9 | import { withLayout } from "../shared/Layout"; 10 | import "./Key.css"; 11 | 12 | const center = pc => 13 | pc ? (pc[0] === "A" || pc[0] === "B" ? pc + 3 : pc + 4) : null; 14 | 15 | const API = { key: Key }; 16 | export default withLayout(API, ({ note, id }) => { 17 | const name = id || "major"; 18 | const tonic = Note.pc(note); 19 | const keyName = tonic + " " + name; 20 | const props = Key.props(keyName); 21 | const major = Key.relative("major", keyName); 22 | const notes = props.intervals.map(transpose(center(tonic))); 23 | return ( 24 |
25 |
key
26 |

{keyName}

27 | 28 |
29 |
30 |

31 | 32 | {props.acc}  33 | 34 | {Key.alteredNotes(keyName).join(" ")} 35 |

36 |
37 |
38 |

39 | 40 | {props.intervals.join(" ")} 41 | 42 | {props.scale.join(" ")} 43 |

44 |
45 |
46 | 47 |
48 |

Scale

49 | 50 |
51 | 52 | 53 | 54 |

Code examples

55 | ${json(props)}`]} /> 56 |
57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/pcset/NameList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PcSet, Chord, Scale, Note } from "tonal"; 3 | import Link from "../shared/Link"; 4 | import CircleSet from "../viz/CircleSet"; 5 | import PianoKeyboard from "../viz/PianoKeyboard"; 6 | import player from "../../player"; 7 | 8 | const SIZE = 40; 9 | 10 | const Row = ({ tonic, name, type, route, sep }) => { 11 | const Set = type === "scale" ? Scale : Chord; 12 | const intervals = Set.intervals(name); 13 | const setchroma = PcSet.chroma(intervals); 14 | const notes = Set.notes(tonic, name); 15 | const chroma = PcSet.chroma(notes); 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | {tonic && ( 23 | 31 | )} 32 | 33 | 34 | {tonic ? tonic + sep + name : name} 35 | 36 | 37 | {tonic ? ( 38 | 44 | ) : null} 45 | 46 | 47 | ); 48 | }; 49 | 50 | /** 51 | * Props: 52 | * - names 53 | * - type 54 | * - tonic 55 | * - route 56 | */ 57 | export default props => { 58 | const sep = props.type === "chord" ? "" : " "; 59 | return ( 60 | 61 | 62 | {props.names.map((name, i) => ( 63 | 70 | ))} 71 | 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Tonal App 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 |
37 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/components/viz/PianoKeyboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Array, Note } from "tonal"; 3 | import "./PianoKeyboard.css"; 4 | import wrap from "classwrap"; 5 | 6 | const WHITES = [0, 2, 4, 5, 7, 9, 11]; 7 | const BLACKS = [1, 3, 6, 8, 10]; 8 | const BPOS = [1, 2, 4, 5, 6]; 9 | const WHITE_WIDTH = 40; 10 | const WHITE_HEIGHT = 150; 11 | const BLACK_WIDTH = 22; 12 | const BLACK_HEIGHT = 90; 13 | 14 | const getKeyTypes = (type, midi, pcset, notes) => { 15 | const chroma = midi % 12; 16 | return wrap([ 17 | "piano-key", 18 | type, 19 | { 20 | active: pcset.chroma[chroma] === "1" || notes.names[midi] !== undefined, 21 | tonic: pcset.tonic === chroma || notes.tonic === midi 22 | } 23 | ]); 24 | }; 25 | 26 | const Key = ({ type, chroma, i, oct, pcset, notes, x, onClick }) => { 27 | const isWhite = type === "white"; 28 | const midi = (oct + 1) * 12 + chroma; 29 | const offset = isWhite 30 | ? i * WHITE_WIDTH 31 | : WHITE_WIDTH * BPOS[i] - BLACK_WIDTH / 2; 32 | 33 | const handleClick = e => { 34 | e.preventDefault(); 35 | onClick(midi, notes.names[midi]); 36 | }; 37 | 38 | return ( 39 | 49 | ); 50 | }; 51 | 52 | const Octave = props => { 53 | return ( 54 | 55 | {WHITES.map((chroma, i) => ( 56 | 57 | ))} 58 | {BLACKS.map((chroma, i) => ( 59 | 60 | ))} 61 | 62 | ); 63 | }; 64 | 65 | export default ({ 66 | className, 67 | setChroma, 68 | setTonic, 69 | width, 70 | tonic, 71 | notes, 72 | onClick, 73 | minOct = 3, 74 | maxOct = 6 75 | }) => { 76 | const pcset = { tonic: setTonic, chroma: setChroma || "" }; 77 | const newnotes = { 78 | tonic: tonic, 79 | names: (notes || []).reduce((index, note) => { 80 | index[Note.midi(note)] = note; 81 | return index; 82 | }, {}) 83 | }; 84 | const octs = Array.range(minOct, maxOct); 85 | // const viewWidth = 1120 86 | const viewWidth = octs.length * 7 * WHITE_WIDTH; 87 | width = width || "100%"; 88 | return ( 89 |
90 | 96 | 97 | {octs.map((o, i) => ( 98 | {})} 105 | /> 106 | ))} 107 | 108 | 109 |
110 | ); 111 | }; 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at danigb@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /public/milligram.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Milligram v1.3.0 3 | * https://milligram.github.io 4 | * 5 | * Copyright (c) 2017 CJ Patoilo 6 | * Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /*# sourceMappingURL=milligram.min.css.map */ -------------------------------------------------------------------------------- /public/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | -------------------------------------------------------------------------------- /public/soundfont-player.min.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&¬e<129?+note:parser.midi(note);var freq=midi?parser.midiToFreq(midi,440):null;if(!freq)return;duration=duration||.2;options=options||{};var destination=options.destination||defaultOptions.destination||ctx.destination;var vcoType=options.vcoType||defaultOptions.vcoType||"sine";var gain=options.gain||defaultOptions.gain||.4;var vco=ctx.createOscillator();vco.type=vcoType;vco.frequency.value=freq;var vca=ctx.createGain();vca.gain.value=gain;vco.connect(vca);vca.connect(destination);vco.start(time);if(duration>0)vco.stop(time+duration);return vco}}Soundfont.noteToMidi=parser.midi;module.exports=Soundfont},{"note-parser":8}],3:[function(require,module,exports){module.exports=ADSR;function ADSR(audioContext){var node=audioContext.createGain();var voltage=node._voltage=getVoltage(audioContext);var value=scale(voltage);var startValue=scale(voltage);var endValue=scale(voltage);node._startAmount=scale(startValue);node._endAmount=scale(endValue);node._multiplier=scale(value);node._multiplier.connect(node);node._startAmount.connect(node);node._endAmount.connect(node);node.value=value.gain;node.startValue=startValue.gain;node.endValue=endValue.gain;node.startValue.value=0;node.endValue.value=0;Object.defineProperties(node,props);return node}var props={attack:{value:0,writable:true},decay:{value:0,writable:true},sustain:{value:1,writable:true},release:{value:0,writable:true},getReleaseDuration:{value:function(){return this.release}},start:{value:function(at){var target=this._multiplier.gain;var startAmount=this._startAmount.gain;var endAmount=this._endAmount.gain;this._voltage.start(at);this._decayFrom=this._decayFrom=at+this.attack;this._startedAt=at;var sustain=this.sustain;target.cancelScheduledValues(at);startAmount.cancelScheduledValues(at);endAmount.cancelScheduledValues(at);endAmount.setValueAtTime(0,at);if(this.attack){target.setValueAtTime(0,at);target.linearRampToValueAtTime(1,at+this.attack);startAmount.setValueAtTime(1,at);startAmount.linearRampToValueAtTime(0,at+this.attack)}else{target.setValueAtTime(1,at);startAmount.setValueAtTime(0,at)}if(this.decay){target.setTargetAtTime(sustain,this._decayFrom,getTimeConstant(this.decay))}}},stop:{value:function(at,isTarget){if(isTarget){at=at-this.release}var endTime=at+this.release;if(this.release){var target=this._multiplier.gain;var startAmount=this._startAmount.gain;var endAmount=this._endAmount.gain;target.cancelScheduledValues(at);startAmount.cancelScheduledValues(at);endAmount.cancelScheduledValues(at);var expFalloff=getTimeConstant(this.release);if(this.attack&&at=end){value=end}return value}},{}],4:[function(require,module,exports){"use strict";function b64ToUint6(nChr){return nChr>64&&nChr<91?nChr-65:nChr>96&&nChr<123?nChr-71:nChr>47&&nChr<58?nChr+4:nChr===43?62:nChr===47?63:0}function decode(sBase64,nBlocksSize){var sB64Enc=sBase64.replace(/[^A-Za-z0-9\+\/]/g,"");var nInLen=sB64Enc.length;var nOutLen=nBlocksSize?Math.ceil((nInLen*3+1>>2)/nBlocksSize)*nBlocksSize:nInLen*3+1>>2;var taBytes=new Uint8Array(nOutLen);for(var nMod3,nMod4,nUint24=0,nOutIdx=0,nInIdx=0;nInIdx>>(16>>>nMod3&24)&255}nUint24=0}}return taBytes}module.exports={decode:decode}},{}],5:[function(require,module,exports){"use strict";module.exports=function(url,type){return new Promise(function(done,reject){var req=new XMLHttpRequest;if(type)req.responseType=type;req.open("GET",url);req.onload=function(){req.status===200?done(req.response):reject(Error(req.statusText))};req.onerror=function(){reject(Error("Network Error"))};req.send()})}},{}],6:[function(require,module,exports){"use strict";var base64=require("./base64");var fetch=require("./fetch");function fromRegex(r){return function(o){return typeof o==="string"&&r.test(o)}}function prefix(pre,name){return typeof pre==="string"?pre+name:typeof pre==="function"?pre(name):name}function load(ac,source,options,defVal){var loader=isArrayBuffer(source)?loadArrayBuffer:isAudioFileName(source)?loadAudioFile:isPromise(source)?loadPromise:isArray(source)?loadArrayData:isObject(source)?loadObjectData:isJsonFileName(source)?loadJsonFile:isBase64Audio(source)?loadBase64Audio:isJsFileName(source)?loadMidiJSFile:null;var opts=options||{};return loader?loader(ac,source,opts):defVal?Promise.resolve(defVal):Promise.reject("Source not valid ("+source+")")}load.fetch=fetch;function isArrayBuffer(o){return o instanceof ArrayBuffer}function loadArrayBuffer(ac,array,options){return new Promise(function(done,reject){ac.decodeAudioData(array,function(buffer){done(buffer)},function(){reject("Can't decode audio data ("+array.slice(0,30)+"...)")})})}var isAudioFileName=fromRegex(/\.(mp3|wav|ogg)(\?.*)?$/i);function loadAudioFile(ac,name,options){var url=prefix(options.from,name);return load(ac,load.fetch(url,"arraybuffer"),options)}function isPromise(o){return o&&typeof o.then==="function"}function loadPromise(ac,promise,options){return promise.then(function(value){return load(ac,value,options)})}var isArray=Array.isArray;function loadArrayData(ac,array,options){return Promise.all(array.map(function(data){return load(ac,data,options,data)}))}function isObject(o){return o&&typeof o==="object"}function loadObjectData(ac,obj,options){var dest={};var promises=Object.keys(obj).map(function(key){if(options.only&&options.only.indexOf(key)===-1)return null;var value=obj[key];return load(ac,value,options,value).then(function(audio){dest[key]=audio})});return Promise.all(promises).then(function(){return dest})}var isJsonFileName=fromRegex(/\.json(\?.*)?$/i);function loadJsonFile(ac,name,options){var url=prefix(options.from,name);return load(ac,load.fetch(url,"text").then(JSON.parse),options)}var isBase64Audio=fromRegex(/^data:audio/);function loadBase64Audio(ac,source,options){var i=source.indexOf(",");return load(ac,base64.decode(source.slice(i+1)).buffer,options)}var isJsFileName=fromRegex(/\.js(\?.*)?$/i);function loadMidiJSFile(ac,name,options){var url=prefix(options.from,name);return load(ac,load.fetch(url,"text").then(midiJsToJson),options)}function midiJsToJson(data){var begin=data.indexOf("MIDI.Soundfont.");if(begin<0)throw Error("Invalid MIDI.js Soundfont format");begin=data.indexOf("=",begin)+2;var end=data.lastIndexOf(",");return JSON.parse(data.slice(begin,end)+"}")}if(typeof module==="object"&&module.exports)module.exports=load;if(typeof window!=="undefined")window.loadAudio=load},{"./base64":4,"./fetch":5}],7:[function(require,module,exports){(function(global){(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var t;if(typeof window!=="undefined"){t=window}else if(typeof global!=="undefined"){t=global}else if(typeof self!=="undefined"){t=self}else{t=this}t.midimessage=e()}})(function(){var e,t,s;return function o(e,t,s){function a(n,i){if(!t[n]){if(!e[n]){var l=typeof require=="function"&&require;if(!i&&l)return l(n,!0);if(r)return r(n,!0);var h=new Error("Cannot find module '"+n+"'");throw h.code="MODULE_NOT_FOUND",h}var c=t[n]={exports:{}};e[n][0].call(c.exports,function(t){var s=e[n][1][t];return a(s?s:t)},c,c.exports,o,e,t,s)}return t[n].exports}var r=typeof require=="function"&&require;for(var n=0;n6?null:C.charAt(t)+f(n)+a(r)}function p(t){if((r(t)||e(t))&&t>=0&&t<128)return+t;var n=i(t);return n&&u(n.midi)?n.midi:null}function s(t,n){var r=p(t);return null===r?null:c(r,n)}function d(t){return(i(t)||{}).letter}function m(t){return(i(t)||{}).acc}function h(t){return(i(t)||{}).pc}function v(t){return(i(t)||{}).step}function g(t){return(i(t)||{}).alt}function x(t){return(i(t)||{}).chroma}function y(t){return(i(t)||{}).oct}var b=/^([a-gA-G])(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)\s*$/,A=[0,2,4,5,7,9,11],C="CDEFGAB";t.regex=o,t.parse=i,t.build=l,t.midi=p,t.freq=s,t.letter=d,t.acc=m,t.pc=h,t.step=v,t.alt=g,t.chroma=x,t.oct=y})},{}],9:[function(require,module,exports){module.exports=function(player){player.on=function(event,cb){if(arguments.length===1&&typeof event==="function")return player.on("event",event);var prop="on"+event;var old=player[prop];player[prop]=old?chain(old,cb):cb;return player};return player};function chain(fn1,fn2){return function(a,b,c,d){fn1(a,b,c,d);fn2(a,b,c,d)}}},{}],10:[function(require,module,exports){"use strict";var player=require("./player");var events=require("./events");var notes=require("./notes");var scheduler=require("./scheduler");var midi=require("./midi");function SamplePlayer(ac,source,options){return midi(scheduler(notes(events(player(ac,source,options)))))}if(typeof module==="object"&&module.exports)module.exports=SamplePlayer;if(typeof window!=="undefined")window.SamplePlayer=SamplePlayer},{"./events":9,"./midi":11,"./notes":12,"./player":13,"./scheduler":14}],11:[function(require,module,exports){var midimessage=require("midimessage");module.exports=function(player){player.listenToMidi=function(input,options){var started={};var opts=options||{};var gain=opts.gain||function(vel){return vel/127};input.onmidimessage=function(msg){var mm=msg.messageType?msg:midimessage(msg);if(mm.messageType==="noteon"&&mm.velocity===0){mm.messageType="noteoff"}if(opts.channel&&mm.channel!==opts.channel)return;switch(mm.messageType){case"noteon":started[mm.key]=player.play(mm.key,0,{gain:gain(mm.velocity)});break;case"noteoff":if(started[mm.key]){started[mm.key].stop();delete started[mm.key]}break}};return player};return player}},{midimessage:7}],12:[function(require,module,exports){"use strict";var note=require("note-parser");var isMidi=function(n){return n!==null&&n!==[]&&n>=0&&n<129};var toMidi=function(n){return isMidi(n)?+n:note.midi(n)};module.exports=function(player){if(player.buffers){var map=player.opts.map;var toKey=typeof map==="function"?map:toMidi;var mapper=function(name){return name?toKey(name)||name:null};player.buffers=mapBuffers(player.buffers,mapper);var start=player.start;player.start=function(name,when,options){var key=mapper(name);var dec=key%1;if(dec){key=Math.floor(key);options=Object.assign(options||{},{cents:Math.floor(dec*100)})}return start(key,when,options)}}return player};function mapBuffers(buffers,toKey){return Object.keys(buffers).reduce(function(mapped,name){mapped[toKey(name)]=buffers[name];return mapped},{})}},{"note-parser":15}],13:[function(require,module,exports){"use strict";var ADSR=require("adsr");var EMPTY={};var DEFAULTS={gain:1,attack:.01,decay:.1,sustain:.9,release:.3,loop:false,cents:0,loopStart:0,loopEnd:0};function SamplePlayer(ac,source,options){var connected=false;var nextId=0;var tracked={};var out=ac.createGain();out.gain.value=1;var opts=Object.assign({},DEFAULTS,options);var player={context:ac,out:out,opts:opts};if(source instanceof AudioBuffer)player.buffer=source;else player.buffers=source;player.start=function(name,when,options){if(player.buffer&&name!==null)return player.start(null,name,when);var buffer=name?player.buffers[name]:player.buffer;if(!buffer){console.warn("Buffer "+name+" not found.");return}else if(!connected){console.warn("SamplePlayer not connected to any node.");return}var opts=options||EMPTY;when=Math.max(ac.currentTime,when||0);player.emit("start",when,name,opts);var node=createNode(name,buffer,opts);node.id=track(name,node);node.env.start(when);node.source.start(when);player.emit("started",when,node.id,node);if(opts.duration)node.stop(when+opts.duration);return node};player.play=function(name,when,options){return player.start(name,when,options)};player.stop=function(when,ids){var node;ids=ids||Object.keys(tracked);return ids.map(function(id){node=tracked[id];if(!node)return null;node.stop(when);return node.id})};player.connect=function(dest){connected=true;out.connect(dest);return player};player.emit=function(event,when,obj,opts){if(player.onevent)player.onevent(event,when,obj,opts);var fn=player["on"+event];if(fn)fn(when,obj,opts)};return player;function track(name,node){node.id=nextId++;tracked[node.id]=node;node.source.onended=function(){var now=ac.currentTime;node.source.disconnect();node.env.disconnect();node.disconnect();player.emit("ended",now,node.id,node)};return node.id}function createNode(name,buffer,options){var node=ac.createGain();node.gain.value=0;node.connect(out);node.env=envelope(ac,options,opts);node.env.connect(node.gain);node.source=ac.createBufferSource();node.source.buffer=buffer;node.source.connect(node);node.source.loop=options.loop||opts.loop;node.source.playbackRate.value=centsToRate(options.cents||opts.cents);node.source.loopStart=options.loopStart||opts.loopStart;node.source.loopEnd=options.loopEnd||opts.loopEnd;node.stop=function(when){var time=when||ac.currentTime;player.emit("stop",time,name);var stopAt=node.env.stop(time);node.source.stop(stopAt)};return node}}function isNum(x){return typeof x==="number"}var PARAMS=["attack","decay","sustain","release"];function envelope(ac,options,opts){var env=ADSR(ac);var adsr=options.adsr||opts.adsr;PARAMS.forEach(function(name,i){if(adsr)env[name]=adsr[i];else env[name]=options[name]||opts[name]});env.value.value=isNum(options.gain)?options.gain:isNum(opts.gain)?opts.gain:1;return env}function centsToRate(cents){return cents?Math.pow(2,cents/1200):1}module.exports=SamplePlayer},{adsr:3}],14:[function(require,module,exports){"use strict";var isArr=Array.isArray;var isObj=function(o){return o&&typeof o==="object"};var OPTS={};module.exports=function(player){player.schedule=function(time,events){var now=player.context.currentTime;var when=time