├── _redirects
├── audio.js
├── Rule.js
├── README.md
├── utils.js
├── LSystem.js
├── Ground3.js
├── Ground5.js
├── Ground4.js
├── Ground1.js
├── modal.js
├── Ground2.js
├── index.html
├── style.css
├── Plant.js
├── sketch.js
└── teoria.js
/_redirects:
--------------------------------------------------------------------------------
1 | https://musical-garden.netlify.app/* https://musicalgarden.eliasjarzombek.com/:splat 301!
2 |
--------------------------------------------------------------------------------
/audio.js:
--------------------------------------------------------------------------------
1 | Tone.context.latencyHint = "playbackrate";
2 | Tone.Transport.start();
3 |
4 | const outputGain = 0.3;
5 | const OUTPUT_NODE = new Tone.Gain(outputGain);
6 | const limiter = new Tone.Limiter();
7 | const compressor = new Tone.Compressor();
8 | OUTPUT_NODE.chain(compressor, limiter, Tone.Destination);
9 |
--------------------------------------------------------------------------------
/Rule.js:
--------------------------------------------------------------------------------
1 | // The Nature of Code
2 | // Daniel Shiffman
3 | // http://natureofcode.com
4 |
5 | // LSystem Rule class
6 |
7 | function Rule(a_, bs_) {
8 | this.a = a_;
9 | this.bVals = bs_;
10 |
11 | this.getA = function () {
12 | return this.a;
13 | };
14 |
15 | // select B based on assigned weights
16 | this.getB = () => {
17 | const indexes = [];
18 | this.bVals &&
19 | this.bVals.forEach(({ value, weight }, index) => {
20 | for (let i = 0; i < weight; i++) {
21 | indexes.push(index);
22 | }
23 | });
24 |
25 | const randomIndex = indexes[Math.floor(Math.random() * indexes.length)];
26 | return this.bVals[randomIndex].value;
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Musical Garden](https://musical-garden.netlify.app/)
2 |
3 | 
4 |
5 | This web instrument allows you to make music by planting and watering different kinds of “audio seeds” that grow into lush melodies and textures.
6 |
7 | Watering the seeds causes them to grow both visually and sonically, and distinct areas in the garden cause the plants to behave in different ways.
8 |
9 | Composing using this interface is more spacial than linear. Plants emanate sound that you navigate through using the mouse, so moving through the space influences the mix of sounds.
10 |
11 | The implementation represents different types of sound using basic geometric forms and generates growth patterns algorithmically using L-Systems — a way of modeling generational systems. These patterns are at times also used to produce melodies.
12 |
13 | The musical garden invites exploration, and can be found at https://musical-garden.netlify.app/
14 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | const mouseIsInDrawArea = () =>
2 | mouseX < width &&
3 | mouseY < height &&
4 | !(mouseX < 280 && mouseY < 90) &&
5 | !(mouseX > width - 80 && mouseY < 80);
6 |
7 | const sign = (p1, p2, p3) => {
8 | return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
9 | };
10 |
11 | // https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle
12 | const pointInTriangle = (v1, v2, v3) => (pt) => {
13 | const d1 = sign(pt, v1, v2);
14 | const d2 = sign(pt, v2, v3);
15 | const d3 = sign(pt, v3, v1);
16 |
17 | const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
18 | const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
19 |
20 | return !(has_neg && has_pos);
21 | };
22 |
23 | const getHue = (h) => Math.floor(h < 0 ? 360 - h : h % 360);
24 |
25 | const getSentence = (ruleset) => {
26 | const { rules, nGenerations, startingLetter } = ruleset;
27 | const lsys = new LSystem(
28 | startingLetter,
29 | rules.map((rls) => new Rule(...rls))
30 | );
31 |
32 | for (let i = 0; i < nGenerations; i++) {
33 | lsys.generate();
34 | }
35 |
36 | return lsys.getSentence();
37 | };
38 |
--------------------------------------------------------------------------------
/LSystem.js:
--------------------------------------------------------------------------------
1 | // The Nature of Code
2 | // Daniel Shiffman
3 | // http://natureofcode.com
4 |
5 | // An LSystem has a starting sentence
6 | // An a ruleset
7 | // Each generation recursively replaces characteres in the sentence
8 | // Based on the rulset
9 |
10 | // Construct an LSystem with a startin sentence and a ruleset
11 | function LSystem(axiom, r) {
12 | this.sentence = axiom; // The sentence (a String)
13 | this.ruleset = r; // The ruleset (an array of Rule objects)
14 | this.generation = 0; // Keeping track of the generation #
15 |
16 | // Generate the next generation
17 | this.generate = function () {
18 | // An empty StringBuffer that we will fill
19 | var nextgen = "";
20 | // For every character in the sentence
21 | for (var i = 0; i < this.sentence.length; i++) {
22 | // What is the character
23 | // We will replace it with itself unless it matches one of our rules
24 | var replace = this.sentence.charAt(i);
25 | // Check every rule
26 | for (var j = 0; j < this.ruleset.length; j++) {
27 | var a = this.ruleset[j].getA();
28 | // if we match the Rule, get the replacement String out of the Rule
29 | if (a === replace) {
30 | replace = this.ruleset[j].getB();
31 | break;
32 | }
33 | }
34 | // Append replacement String
35 | nextgen += replace;
36 | }
37 | // Replace sentence
38 | this.sentence = nextgen;
39 | // Increment generation
40 | this.generation++;
41 | };
42 |
43 | this.getSentence = function () {
44 | return this.sentence;
45 | };
46 |
47 | this.getGeneration = function () {
48 | return this.generation;
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/Ground3.js:
--------------------------------------------------------------------------------
1 | function Ground3(color) {
2 | this.color = color;
3 | this.outputNode = new Tone.Gain().connect(OUTPUT_NODE);
4 |
5 | const musicScale = teoria.note("G4").scale("dorian");
6 | const notes = musicScale.notes();
7 | this.getSynth = (waveform) => {
8 | return new Tone.FMSynth({
9 | volume: -Infinity,
10 | oscillator: { type: waveform },
11 | modulationIndex: 0,
12 | });
13 | };
14 |
15 | this.renderGround = ({ x, y }, ctx_) => {
16 | const ctx = ctx_ || window;
17 | const [h, s, l] = this.color;
18 | ctx.fill(h, s, l + noise(x, y) * 6);
19 | const w = 100 + random(-15, 15);
20 | ctx.rect(x, y, w, w);
21 | };
22 |
23 | this.getDetune = (noiseVal) => {
24 | return 0;
25 | };
26 |
27 | let duration = 0;
28 | const part = new Tone.Part((time, value) => {
29 | const { synth, note, velocity, getNoteDuration } = value;
30 | synth.triggerAttackRelease(note, getNoteDuration(), time, velocity);
31 | }, []).start(1);
32 | part.loop = true;
33 |
34 | this.getPart = ({ sentence, synth, getState }) => {
35 | let noteDuration = 0.1;
36 | // synth.triggerAttack("A1");
37 | let modulationIndex = 0;
38 | const noteIndex = Math.floor(
39 | map(mouseY, 0, height, -2 * notes.length, 2 * notes.length)
40 | );
41 | const time = map(mouseX, 0, width, 0, 1);
42 | const startX = mouseX;
43 | part.add({
44 | synth,
45 | time,
46 | note: musicScale.get(noteIndex).toString(),
47 | velocity: 1,
48 | getNoteDuration: () => noteDuration,
49 | });
50 | part.loopEnd = 1;
51 |
52 | const part2 = new Tone.Loop((time) => {
53 | const { isGrowing } = getState();
54 | if (isGrowing) {
55 | noteDuration += 0.005;
56 | modulationIndex++;
57 | synth.set({ modulationIndex });
58 | noteDuration = constrain(noteDuration, 0.01, 1);
59 | } else {
60 | modulationIndex--;
61 | }
62 | modulationIndex = constrain(modulationIndex, 0, 20);
63 | }, 0.1);
64 | part2.start(1);
65 |
66 | return {
67 | getWiggle: () => {
68 | const startPos = map(startX, 0, width, 0, 1);
69 | const percent = part.progress + startPos;
70 | // rect(startPos, 10, 100, 20);
71 | return (sin(percent * 2 * PI) + 1) * 50;
72 | },
73 | };
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/Ground5.js:
--------------------------------------------------------------------------------
1 | function Ground5(color) {
2 | this.color = color;
3 | this.outputNode = new Tone.Gain();
4 | this.outputNode2 = new Tone.Gain().chain(
5 | new Tone.Filter({ type: "lowpass", frequency: 1000 }),
6 | new Tone.Reverb({ decay: 10 }),
7 | OUTPUT_NODE
8 | );
9 |
10 | this.effects = new Tone.Vibrato(20, 1);
11 | const effectsChannel = new Tone.Channel();
12 | effectsChannel.receive("ground-5-send");
13 | effectsChannel.chain(this.effects, OUTPUT_NODE);
14 |
15 | this.getSynth = (waveform) => {
16 | return new Tone.MonoSynth({
17 | volume: -Infinity,
18 | oscillator: { type: waveform },
19 | envelope: { attack: 0.01 },
20 | filter: {
21 | type: "lowpass",
22 | frequency: 20000,
23 | rolloff: -12,
24 | Q: 1,
25 | gain: 0,
26 | },
27 | filterEnvelope: {
28 | attack: 0.1,
29 | baseFrequency: 20000,
30 | decay: 0.2,
31 | exponent: 2,
32 | octaves: 3,
33 | release: 2,
34 | sustain: 0.5,
35 | },
36 | });
37 | };
38 |
39 | this.renderGround = ({ x, y }, ctx_) => {
40 | const ctx = ctx_ || window;
41 | const [h, s, l] = this.color;
42 | ctx.fill(h, s, l + noise(x, y) * 16);
43 | ctx.rectMode(CENTER);
44 | ctx.rect(x, y, 100, 100);
45 | };
46 |
47 | this.getDetune = (noiseVal) => {
48 | return noiseVal * 10;
49 | };
50 |
51 | this.getPart = ({ sentence, synthOutput, synth, getState }) => {
52 | this.effects.set({ frequency: random(0.1, 10) });
53 |
54 | const sendChannel = new Tone.Channel();
55 | sendChannel.send("ground-5-send");
56 | // sendChannel.connect(effectsChannel);
57 | const dryOut = new Tone.Channel().connect(this.outputNode2);
58 | synthOutput.connect(sendChannel);
59 | synthOutput.fan(sendChannel, dryOut);
60 |
61 | synth.set({
62 | envelope: { attack: 2 },
63 | });
64 |
65 | synth.triggerAttack(parseInt(random(20, 1000)));
66 |
67 | const part = new Tone.Loop((time) => {
68 | const { isGrowing } = getState();
69 | if (isGrowing) {
70 | // sendAmount = 0;
71 | // sendAmount = constrain(sendAmount, -60, 0);
72 | sendChannel.volume.rampTo(0, 0.5);
73 | dryOut.volume.rampTo(-Infinity, 0.5);
74 | } else {
75 | sendChannel.volume.rampTo(-Infinity, 0.5);
76 | dryOut.volume.rampTo(0, 0.5);
77 | }
78 | }, 0.2);
79 | part.start();
80 | return part;
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/Ground4.js:
--------------------------------------------------------------------------------
1 | function Ground4(color) {
2 | const musicScale = teoria.note("D").scale("minor");
3 | const notes = musicScale.notes();
4 | this.wiggleAmt = 0;
5 | this.color = color;
6 | this.outputNode = new Tone.Gain();
7 | this.outputNode2 = new Tone.Gain().chain(
8 | new Tone.Filter({ type: "lowpass", frequency: 14000 }),
9 | new Tone.Reverb({ decay: 10 }),
10 | OUTPUT_NODE
11 | );
12 |
13 | this.effects = new Tone.Distortion({ distortion: 0.1, wet: 0.5 });
14 | const reverb = new Tone.Reverb({ decay: 10, wet: 0.1 });
15 | const chorus = new Tone.Chorus();
16 | const effectsChannel = new Tone.Channel();
17 | effectsChannel.receive("ground-4-send");
18 | effectsChannel.chain(this.effects, chorus, reverb, OUTPUT_NODE);
19 |
20 | this.getSynth = (waveform) => {
21 | return new Tone.MembraneSynth({
22 | volume: -Infinity,
23 | oscillator: { type: waveform },
24 | });
25 | };
26 |
27 | this.renderGround = ({ x, y }, ctx_) => {
28 | const ctx = ctx_ || window;
29 | const [h, s, l] = this.color;
30 | ctx.fill(h, s, l + noise(x, y) * 6);
31 | ctx.beginShape();
32 | ctx.vertex(x, y + 150);
33 | ctx.vertex(x + 100, y);
34 | ctx.vertex(x, y - 150);
35 | ctx.vertex(x - 50, y);
36 | ctx.endShape(CLOSE);
37 | };
38 |
39 | this.getDetune = (noiseVal) => {
40 | return noiseVal / 2;
41 | };
42 |
43 | this.getWiggleAmt = () => {
44 | return this.wiggleAmt;
45 | };
46 |
47 | this.getPart = ({ sentence, synthOutput, synth, getState }) => {
48 | this.effects.set({ frequency: random(0.1, 10) });
49 | let note = musicScale.get(Math.floor(random(0, notes.length * 3)));
50 | const durOptions = [0.5, 1 / 3, 2 / 3, 5 / 4, 2 / 5, 1, 2, 3, 4];
51 | const loopDuration = durOptions[Math.floor(random(0, durOptions.length))];
52 | const sendChannel = new Tone.Channel({ volume: 5 });
53 | let wiggle = 0;
54 | sendChannel.send("ground-4-send");
55 | // sendChannel.connect(effectsChannel);
56 | const dryOut = new Tone.Channel().connect(this.outputNode2);
57 | synthOutput.connect(sendChannel);
58 | synthOutput.fan(sendChannel, dryOut);
59 |
60 | synth.set({
61 | envelope: {
62 | attack: 0.001,
63 | decay: 1.4,
64 | release: 0.2,
65 | },
66 | });
67 |
68 | sendChannel.volume.rampTo(0, 0.5);
69 | dryOut.volume.rampTo(-Infinity, 0.5);
70 | const part = new Tone.Loop((time) => {
71 | wiggle = random(11, 800);
72 | synth.triggerAttack(note.fq() / 2, time);
73 | const { isGrowing } = getState();
74 | if (isGrowing) {
75 | synth.set({ pitchDecay: 0.5 });
76 | note = musicScale.get(Math.floor(random(0, notes.length * 3)));
77 | } else {
78 | synth.set({ pitchDecay: 0.05 });
79 | }
80 | }, loopDuration);
81 |
82 | part.start(0);
83 |
84 | return {
85 | getWiggle: () => wiggle,
86 | getStemWiggle: () => 1,
87 | };
88 | };
89 | }
90 |
--------------------------------------------------------------------------------
/Ground1.js:
--------------------------------------------------------------------------------
1 | function Ground1(color) {
2 | this.color = color;
3 | const musicScale = teoria.note("G4").scale("dorian");
4 | const notes = musicScale.notes();
5 |
6 | const getRandomNote = () =>
7 | musicScale.get(Math.floor(random(0, notes.length * 2)));
8 |
9 | this.outputNode = new Tone.Gain(0.1);
10 | const compressor = new Tone.Compressor();
11 | this.outputNode.chain(compressor, OUTPUT_NODE);
12 |
13 | this.getSynth = (waveform) => {
14 | return new Tone.MonoSynth({
15 | volume: -Infinity,
16 | oscillator: { type: waveform },
17 | envelope: { attack: 0.01 },
18 | filter: {
19 | type: "lowpass",
20 | frequency: 20000,
21 | rolloff: -12,
22 | Q: 1,
23 | gain: 0,
24 | },
25 | filterEnvelope: {
26 | attack: 0.1,
27 | baseFrequency: 20000,
28 | decay: 0.2,
29 | exponent: 2,
30 | octaves: 3,
31 | release: 2,
32 | sustain: 0.5,
33 | },
34 | });
35 | };
36 |
37 | this.renderGround = ({ x, y }, ctx_) => {
38 | const ctx = ctx_ || window;
39 | const [h, s, l] = this.color;
40 | ctx.fill(h, s, l + noise(x, y) * 6);
41 | const r = 100 + random(-15, 15);
42 | ctx.ellipse(x, y, r, r);
43 | };
44 |
45 | this.getDetune = (noiseVal) => {
46 | // const val = noiseVal;
47 | return 0;
48 | };
49 |
50 | this.getPart = ({ sentence, synth, getState }) => {
51 | synth.set({
52 | envelope: { attack: 0.4, release: 0.4 },
53 | filter: { type: "lowpass", frequency: 15000, gain: 1 },
54 | filterEnvelope: { baseFrequency: 15000, attack: 0.3 },
55 | });
56 |
57 | const { growSpeed } = getState();
58 | let currLetterIndex = 0;
59 | let currNoteIndex = Math.floor(random(0, notes.length * 3) / 2);
60 |
61 | const part = new Tone.Loop((time) => {
62 | const { isGrowing, isDoneGrowing, maxLetters } = getState();
63 | const t = time + 0.1;
64 | console.log(t);
65 | if (maxLetters === 0) {
66 | const note = musicScale.get(currNoteIndex);
67 |
68 | synth.triggerAttackRelease(note.toString(), t, 0.5);
69 | } else if (!isGrowing) {
70 | const currLetter = sentence[currLetterIndex];
71 | // if (currLetterIndex === 0) {
72 | // currNoteIndex = 0;
73 | // }
74 | if (currLetter === "-") {
75 | currNoteIndex--;
76 | }
77 | if (currLetter === "+") {
78 | currNoteIndex++;
79 | }
80 | const note = musicScale.get(currNoteIndex);
81 | // console.log(currNoteIndex);
82 | synth.triggerAttackRelease(note.toString(), t, 0.5);
83 |
84 | currLetterIndex++;
85 | // currLetterIndex = currLetterIndex % maxLetters;
86 | } else {
87 | if (!isDoneGrowing) {
88 | currNoteIndex = Math.floor(random(0, notes.length * 3) / 2);
89 | synth.triggerAttackRelease(
90 | musicScale.get(currNoteIndex).toString(),
91 | t,
92 | 0.1
93 | );
94 | }
95 | }
96 | }, 1 / (5 * growSpeed));
97 |
98 | part.start(1);
99 |
100 | return part;
101 | };
102 | }
103 |
--------------------------------------------------------------------------------
/modal.js:
--------------------------------------------------------------------------------
1 | var modal = function () {
2 | /**
3 | * Element.closest() polyfill
4 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
5 | */
6 | if (!Element.prototype.closest) {
7 | if (!Element.prototype.matches) {
8 | Element.prototype.matches =
9 | Element.prototype.msMatchesSelector ||
10 | Element.prototype.webkitMatchesSelector;
11 | }
12 | Element.prototype.closest = function (s) {
13 | var el = this;
14 | var ancestor = this;
15 | if (!document.documentElement.contains(el)) return null;
16 | do {
17 | if (ancestor.matches(s)) return ancestor;
18 | ancestor = ancestor.parentElement;
19 | } while (ancestor !== null);
20 | return null;
21 | };
22 | }
23 |
24 | //
25 | // Settings
26 | //
27 | var settings = {
28 | speedOpen: 50,
29 | speedClose: 250,
30 | activeClass: "is-active",
31 | visibleClass: "is-visible",
32 | selectorTarget: "[data-modal-target]",
33 | selectorTrigger: "[data-modal-trigger]",
34 | selectorClose: "[data-modal-close]",
35 | };
36 |
37 | //
38 | // Methods
39 | //
40 |
41 | // Toggle accessibility
42 | var toggleccessibility = function (event) {
43 | if (event.getAttribute("aria-expanded") === "true") {
44 | event.setAttribute("aria-expanded", false);
45 | } else {
46 | event.setAttribute("aria-expanded", true);
47 | }
48 | };
49 |
50 | // Open Modal
51 | var openModal = function (trigger) {
52 | // Find target
53 | var target = document.getElementById(trigger.getAttribute("aria-controls"));
54 |
55 | // Make it active
56 | target.classList.add(settings.activeClass);
57 |
58 | // Make body overflow hidden so it's not scrollable
59 | document.documentElement.style.overflow = "hidden";
60 |
61 | // Toggle accessibility
62 | toggleccessibility(trigger);
63 |
64 | // Make it visible
65 | setTimeout(function () {
66 | target.classList.add(settings.visibleClass);
67 | }, settings.speedOpen);
68 | };
69 |
70 | // Close Modal
71 | var closeModal = function (event) {
72 | // Find target
73 | var closestParent = event.closest(settings.selectorTarget),
74 | childrenTrigger = document.querySelector(
75 | '[aria-controls="' + closestParent.id + '"'
76 | );
77 |
78 | // Make it not visible
79 | closestParent.classList.remove(settings.visibleClass);
80 |
81 | // Remove body overflow hidden
82 | document.documentElement.style.overflow = "";
83 |
84 | // Toggle accessibility
85 | toggleccessibility(childrenTrigger);
86 |
87 | // Make it not active
88 | setTimeout(function () {
89 | closestParent.classList.remove(settings.activeClass);
90 | }, settings.speedClose);
91 | };
92 |
93 | // Click Handler
94 | var clickHandler = function (event) {
95 | // Find elements
96 | var toggle = event.target,
97 | open = toggle.closest(settings.selectorTrigger),
98 | close = toggle.closest(settings.selectorClose);
99 |
100 | // Open modal when the open button is clicked
101 | if (open) {
102 | openModal(open);
103 | }
104 |
105 | // Close modal when the close button (or overlay area) is clicked
106 | if (close) {
107 | closeModal(close);
108 | }
109 |
110 | // Prevent default link behavior
111 | if (open || close) {
112 | event.preventDefault();
113 | }
114 | event.stopPropagation();
115 | };
116 |
117 | // Keydown Handler, handle Escape button
118 | var keydownHandler = function (event) {
119 | if (event.key === "Escape" || event.keyCode === 27) {
120 | // Find all possible modals
121 | var modals = document.querySelectorAll(settings.selectorTarget),
122 | i;
123 |
124 | // Find active modals and close them when escape is clicked
125 | for (i = 0; i < modals.length; ++i) {
126 | if (modals[i].classList.contains(settings.activeClass)) {
127 | closeModal(modals[i]);
128 | }
129 | }
130 | }
131 | };
132 |
133 | //
134 | // Inits & Event Listeners
135 | //
136 | document.addEventListener("click", clickHandler, false);
137 | document.addEventListener("keydown", keydownHandler, false);
138 | };
139 |
140 | modal();
141 |
--------------------------------------------------------------------------------
/Ground2.js:
--------------------------------------------------------------------------------
1 | function Ground2(color) {
2 | this.color = color;
3 |
4 | let currentChordRoot = 1;
5 | this.reverb = new Tone.FeedbackDelay(0.4, 0.5);
6 | const reverbChannel = new Tone.Channel();
7 | reverbChannel.receive("ground-2-send");
8 | reverbChannel.chain(this.reverb, OUTPUT_NODE);
9 |
10 | this.outputNode = new Tone.Gain(2);
11 | this.outputNode.chain(OUTPUT_NODE);
12 |
13 | const musicScale = teoria.note("D").scale("minor");
14 | const notes = musicScale.notes();
15 |
16 | const updateChordLoop = new Tone.Loop((time) => {
17 | if (Math.random() > 0.9) {
18 | currentChordRoot = Math.floor(random(0, 8));
19 | }
20 | }, 1);
21 |
22 | updateChordLoop.start(0);
23 |
24 | const getChord = () => [
25 | musicScale.get(currentChordRoot),
26 | musicScale.get(currentChordRoot + 2),
27 | musicScale.get(currentChordRoot + 4),
28 | musicScale.get(currentChordRoot + 6),
29 | ];
30 |
31 | this.getSynth = (waveform) => {
32 | return new Tone.MonoSynth({
33 | volume: -Infinity,
34 | oscillator: { type: waveform },
35 | envelope: { attack: 0.01 },
36 | filter: {
37 | type: "lowpass",
38 | frequency: 20000,
39 | rolloff: -12,
40 | Q: 1,
41 | gain: 0,
42 | },
43 | filterEnvelope: {
44 | attack: 0.1,
45 | baseFrequency: 20000,
46 | decay: 0.2,
47 | exponent: 2,
48 | octaves: 3,
49 | release: 2,
50 | sustain: 0.5,
51 | },
52 | });
53 | };
54 |
55 | this.renderGround = ({ x, y }, ctx_) => {
56 | const ctx = ctx_ || window;
57 | ctx.beginShape();
58 | const [h, s, l] = this.color;
59 | ctx.fill(h, s, l + noise(x, y) * 6);
60 | ctx.vertex(x, y + 100);
61 | ctx.vertex(x + 90, y + 100);
62 | ctx.vertex(x + 50, y - 20);
63 | ctx.endShape(CLOSE);
64 | };
65 |
66 | this.getDetune = (noiseVal) => {
67 | const val = noiseVal * 0.6;
68 | return val - val / 2;
69 | };
70 |
71 | this.getPart = ({ sentence, synth, synthOutput, getState }) => {
72 | const { growSpeed } = getState();
73 | let sendAmount = -60;
74 |
75 | let filterFreq = 50;
76 |
77 | const sendChannel = new Tone.Channel();
78 | sendChannel.send("ground-2-send", sendAmount);
79 | sendChannel.connect(reverbChannel);
80 |
81 | synthOutput.connect(sendChannel);
82 |
83 | synth.set({
84 | portamento: random(0, 1),
85 | envelope: { attack: 0.4 },
86 | filter: { type: "lowpass", frequency: filterFreq, gain: 1, q: 15 },
87 | filterEnvelope: { baseFrequency: filterFreq },
88 | });
89 |
90 | const chordIndex = Math.floor(random(0, getChord().length));
91 |
92 | const octave = Math.floor(random(1, 4));
93 |
94 | let currLetterIndex = 0;
95 | let currNoteIndex = Math.floor(
96 | random(0, musicScale.notes().length * 3) / 2
97 | );
98 |
99 | const part = new Tone.Loop((time) => {
100 | const note = getChord()[chordIndex];
101 | const { isGrowing, isDoneGrowing, maxLetters } = getState();
102 | synth.triggerAttack((note.fq() * 2) / octave, time + 0.1, 0.1);
103 |
104 | if (isDoneGrowing) {
105 | sendAmount += 5;
106 | sendAmount = constrain(sendAmount, -60, 0);
107 | sendChannel.send("ground-2-send", sendAmount);
108 | }
109 |
110 | if (!isGrowing) {
111 | const currLetter = sentence[currLetterIndex];
112 | if (currLetterIndex === 0) {
113 | // currNoteIndex = 0;
114 | }
115 | if (currLetter === "-") {
116 | currNoteIndex--;
117 | }
118 | if (currLetter === "+") {
119 | currNoteIndex++;
120 | }
121 |
122 | currLetterIndex++;
123 | currLetterIndex = currLetterIndex % maxLetters;
124 | } else {
125 | // while being watered
126 | filterFreq *= 2;
127 | filterFreq = constrain(filterFreq, 20, 20000);
128 | synth.set({
129 | filter: { type: "lowpass", frequency: filterFreq },
130 | filterEnvelope: { baseFrequency: filterFreq },
131 | });
132 | if (!isDoneGrowing) {
133 | // synth.triggerAttackRelease(
134 | // getRandomNoteInChord().toString(),
135 | // time + 0.1,
136 | // 0.1
137 | // );
138 | }
139 | }
140 | }, 1 / growSpeed);
141 |
142 | part.start();
143 | return part;
144 | };
145 | }
146 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Musical Garden
4 |
5 |
9 |
10 |
11 | ?
19 |
31 |
32 |
33 |
34 |
35 |
43 |
44 |
45 | This web instrument allows you to make music by planting and
46 | watering different kinds of “audio seeds” that grow into lush
47 | melodies and textures.
48 |
49 |
50 | Watering the seeds causes them to grow both visually and sonically,
51 | and distinct areas in the garden cause the plants to behave in
52 | different ways.
53 |
54 |
55 | Composing using this interface is more spacial than linear. Plants
56 | emanate sound that you navigate through using the mouse, so moving
57 | through the space influences the mix of sounds.
58 |
59 |
60 | The implementation represents different types of sound using basic
61 | geometric forms and generates growth patterns algorithmically using
62 | L-Systems — a way of modeling generational systems. These patterns
63 | are at times also used to produce melodies.
64 |
65 |
The musical garden invites exploration...
66 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
82 |
87 |
92 |
97 |
102 |
103 |
108 |
113 |
118 |
123 |
128 |
133 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | font-family: "Roboto Mono", sans-serif;
4 | }
5 |
6 | html,
7 | body {
8 | margin: 0;
9 | padding: 0;
10 | }
11 | canvas {
12 | display: block;
13 | }
14 |
15 | .controls {
16 | display: flex;
17 | position: absolute;
18 | }
19 |
20 | .btn {
21 | cursor: pointer;
22 | width: 40px;
23 | height: 40px;
24 | /* padding: 15px; */
25 | margin: 0;
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | margin: 10px;
30 | background: none;
31 | transition: all 0s;
32 | position: relative;
33 | border: none;
34 | border-radius: 2px;
35 | border-width: 0px;
36 | border-color: hsla(0, 0, 0, 0);
37 | border-style: solid;
38 | /* stroke-dashoffset: 100; */
39 | stroke-dasharray: 0;
40 | }
41 |
42 | .btn--label {
43 | position: absolute;
44 | bottom: -20px;
45 | opacity: 0.8;
46 | }
47 |
48 | .btn:hover .btn--label,
49 | .btn.isActive .btn--label {
50 | bottom: -22px;
51 | }
52 |
53 | .btn.isActive,
54 | .btn:hover {
55 | border-width: 2px;
56 | margin-bottom: 8px;
57 | }
58 |
59 | .btn--circle:after {
60 | background-color: hsl(80, 50%, 30%);
61 | }
62 |
63 | .circle {
64 | display: inline-block;
65 | width: 30px;
66 | height: 30px;
67 | border-radius: 50%;
68 | background: hsl(80, 50%, 30%);
69 | }
70 |
71 | .btn--square:after {
72 | background-color: hsl(30, 50%, 50%);
73 | }
74 | .square {
75 | display: inline-block;
76 | width: 27px;
77 | height: 27px;
78 | background: hsl(30, 50%, 50%);
79 | }
80 | /*
81 | .btn--water:after {
82 | background-color: hsl(190, 20%, 65%);
83 | } */
84 |
85 | .btn--water svg {
86 | fill: currentColor;
87 | position: absolute;
88 | width: 22px;
89 | }
90 | /*
91 | .btn--water:before {
92 | display: absolute;
93 | content: url('data:image/svg+xml;charset=UTF-8, ');
94 | background-size: 50px 50px;
95 | width: 50px;
96 | height: 50px;
97 | } */
98 |
99 | .info {
100 | color: #eee;
101 | position: absolute;
102 | bottom: 0;
103 | right: 0;
104 | padding: 5px;
105 | font-size: 12px;
106 | opacity: 0.8;
107 | }
108 | .info:hover {
109 | opacity: 1;
110 | }
111 | .info a {
112 | color: #eee;
113 | }
114 |
115 | .more-info {
116 | position: absolute;
117 | right: 20px;
118 | top: 20px;
119 | color: white;
120 | background: none;
121 | border: 2px solid white;
122 | border-radius: 50%;
123 | width: 40px;
124 | height: 40px;
125 | text-align: center;
126 | font-size: 20px;
127 | cursor: pointer;
128 | text-decoration: none;
129 | display: flex;
130 | justify-content: center;
131 | align-items: center;
132 | }
133 |
134 | .project-description {
135 | position: absolute;
136 | top: 100px;
137 | max-width: 530px;
138 | margin: 0 auto;
139 | left: 0;
140 | right: 0;
141 | background-color: white;
142 | padding: 25px 40px;
143 | box-shadow: 0 2px 30px -3px rgba(0, 0, 0, 0.2);
144 | }
145 | h1 {
146 | margin: 0;
147 | }
148 | /* =============== modal ================ */
149 | .modal {
150 | display: none;
151 | }
152 | .modal__overlay {
153 | position: fixed;
154 | top: 0;
155 | right: 0;
156 | bottom: 0;
157 | left: 0;
158 | width: 100%;
159 | z-index: 200;
160 | opacity: 0;
161 |
162 | transition: opacity 0.2s;
163 | will-change: opacity;
164 | background-color: #000;
165 |
166 | -webkit-user-select: none;
167 | -moz-user-select: none;
168 | -ms-user-select: none;
169 | user-select: none;
170 | }
171 |
172 | .modal__header {
173 | /* Optional */
174 | padding: 1.5rem;
175 | display: flex;
176 | justify-content: space-between;
177 | align-items: center;
178 | border-bottom: 1px solid #ddd;
179 | }
180 |
181 | .modal__close {
182 | /* Optional */
183 | margin: 0;
184 | padding: 0;
185 | border: none;
186 | background-color: transparent;
187 | cursor: pointer;
188 | background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='16px' viewBox='0 0 15 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='2.-Menu' transform='translate(-15.000000, -13.000000)' stroke='%23000000'%3E%3Cg id='Group' transform='translate(15.000000, 13.521000)'%3E%3Cpath d='M0,0.479000129 L15,14.2971819' id='Path-3'%3E%3C/path%3E%3Cpath d='M0,14.7761821 L15,-1.24344979e-14' id='Path-3'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
189 | width: 15px;
190 | height: 15px;
191 | }
192 |
193 | .modal__wrapper {
194 | width: 100%;
195 | z-index: 9999;
196 | overflow: auto;
197 | opacity: 0;
198 |
199 | max-width: 540px;
200 | max-height: 80vh;
201 |
202 | transition: transform 0.2s, opacity 0.2s;
203 | will-change: transform;
204 | background-color: #fff;
205 |
206 | display: flex;
207 | flex-direction: column;
208 |
209 | -webkit-transform: translateY(5%);
210 | transform: translateY(5%);
211 |
212 | -webkit-overflow-scrolling: touch; /* enables momentum scrolling in iOS overflow elements */
213 |
214 | /* Optional */
215 | box-shadow: 0 2px 6px #777;
216 | border-radius: 2px;
217 | margin: 20px;
218 | }
219 |
220 | .modal__content {
221 | position: relative;
222 | overflow-x: hidden;
223 | overflow-y: auto;
224 | height: 100%;
225 | flex-grow: 1;
226 | /* Optional */
227 | padding: 1.5rem;
228 | }
229 |
230 | .modal.is-active {
231 | display: flex;
232 | justify-content: center;
233 | align-items: center;
234 | position: fixed;
235 | top: 0;
236 | right: 0;
237 | left: 0;
238 | bottom: 0;
239 | z-index: 9999;
240 | }
241 |
242 | .modal.is-visible .modal__wrapper {
243 | opacity: 1;
244 | -webkit-transform: translateY(0);
245 | transform: translateY(0);
246 | }
247 |
248 | .modal.is-visible .modal__overlay {
249 | opacity: 0.5;
250 | }
251 |
--------------------------------------------------------------------------------
/Plant.js:
--------------------------------------------------------------------------------
1 | function Plant(props) {
2 | const {
3 | ruleset,
4 | startPos,
5 | theta,
6 | baseColor,
7 | waveform,
8 | drawSeed,
9 | ground,
10 | } = props;
11 |
12 | const { segmentLength } = ruleset;
13 |
14 | let maxLetters = 0;
15 | let framesPerStep = parseInt(segmentLength / 8);
16 | let growSpeed = 1;
17 | let isDoneGrowing = false;
18 | let isGrowing = true;
19 | this.count = 0;
20 |
21 | const panner = new Tone.Panner3D({
22 | panningModel: "HRTF",
23 | positionX: startPos.x,
24 | positionY: startPos.y,
25 | distanceModel: "linear",
26 | maxDistance: 500,
27 | });
28 |
29 | const meter = new Tone.Meter();
30 |
31 | const synth = ground.getSynth(waveform);
32 |
33 | const synthOutput = new Tone.Gain();
34 | synth.chain(
35 | new Tone.Gain(0.5),
36 | panner,
37 | synthOutput,
38 | meter,
39 | ground.outputNode
40 | );
41 | synth.volume.linearRampTo(0.9, 0.4);
42 |
43 | const sentence = getSentence(ruleset);
44 |
45 | const { getWiggle, getStemWiggle } =
46 | ground.getPart({
47 | sentence,
48 | synth,
49 | synthOutput,
50 | getState: () => ({
51 | growSpeed,
52 | isGrowing,
53 | isDoneGrowing,
54 | maxLetters,
55 | }),
56 | }) || {};
57 |
58 | const gr = createGraphics(width, height);
59 | const randomWidths = sentence.split("").map((_) => random(2, 15));
60 | // let noiseAmt = 500;
61 | let wiggleAmt = 500;
62 |
63 | const [h, s, l] = baseColor;
64 | const stemHue = getHue(h + (random(12) - 6));
65 | const leafHue = getHue(h + (random(20) - 10));
66 | const fruitHue = getHue(h + random(100, 180));
67 |
68 | const mouseMaxDistance = 300;
69 | let isStartPhase = false;
70 |
71 | let saturationMultiplier = 1;
72 | if (ground.color[1] === 0) {
73 | saturationMultiplier = 0;
74 | }
75 |
76 | this.render = function () {
77 | const mouseD = dist(mouseX, mouseY, startPos.x, startPos.y);
78 | let lightness = map(mouseD, 0, mouseMaxDistance, 10, -5);
79 | lightness = constrain(lightness, -5, 10);
80 | const mouseDiffX = Math.abs(mouseX - startPos.x);
81 | const mouseDiffY = mouseY - startPos.y;
82 | const isInColumn = mouseDiffX < 25 && mouseDiffY < 0 && mouseDiffY > -200;
83 | const isWatering = activeTool === "water" && isInColumn && mouseIsPressed;
84 | let w = 5;
85 |
86 | isGrowing = isStartPhase || isWatering;
87 |
88 | if (this.count > 0) {
89 | isStartPhase = false;
90 | }
91 |
92 | if (mouseD > mouseMaxDistance && !isGrowing) {
93 | image(gr, 0, 0, width, height);
94 | return;
95 | }
96 |
97 | gr.resetMatrix();
98 | gr.clear();
99 |
100 | gr.noiseSeed(startPos.x);
101 | gr.translate(startPos.x, startPos.y);
102 |
103 | // const widthAdd = map(meter.getValue(), -50, 0, -1, 1);
104 | const radiusScale = map(meter.getValue(), -20, 0, 0.5, 1.2);
105 | gr.push();
106 | gr.scale(1);
107 | // console.log(meter.getValue());
108 | gr.fill(
109 | lerpColor(
110 | color(
111 | ground.color[0],
112 | ground.color[1] * saturationMultiplier,
113 | ground.color[2]
114 | ),
115 | color(stemHue, (s - 10) * saturationMultiplier, l + lightness),
116 | 0.4
117 | )
118 | );
119 |
120 | gr.stroke(stemHue, s * saturationMultiplier, l);
121 | gr.strokeWeight(2);
122 | gr.scale(constrain(radiusScale, 0.5, 1.2));
123 | drawSeed(0, 0, gr);
124 | gr.pop();
125 | gr.rotate(-PI / 2);
126 |
127 | if (saturationMultiplier === 0) {
128 | if (isWatering) {
129 | wiggleAmt += 5;
130 | } else {
131 | wiggleAmt -= 5;
132 | }
133 | wiggleAmt = constrain(wiggleAmt, 10, 600);
134 | } else {
135 | if (isWatering) {
136 | wiggleAmt += 1;
137 | } else {
138 | wiggleAmt -= 1;
139 | }
140 | wiggleAmt = constrain(wiggleAmt, 10, 100);
141 | }
142 |
143 | const noiseVal = noise(this.count / 50) * (wiggleAmt / 50);
144 |
145 | wiggleAmt = wiggleAmt * 1 - map(mouseD, 0, mouseMaxDistance, 0, 1);
146 | if (getWiggle) {
147 | wiggleAmt = getWiggle();
148 | }
149 | if (synth.detune) {
150 | synth.detune.value = ground.getDetune(noiseVal);
151 | }
152 | gr.colorMode(HSL);
153 |
154 | for (let i = 0; i < maxLetters; i++) {
155 | const c = sentence[i];
156 | const indexNoiseVal =
157 | (getStemWiggle && getStemWiggle()) || noise(this.count / 80, i) - 0.5;
158 | const angle = radians(theta + indexNoiseVal * wiggleAmt * (1 / w));
159 | if (c === "F" || c === "G") {
160 | const stemColor = gr.lerpColor(
161 | color(
162 | ground.color[0],
163 | ground.color[1] * saturationMultiplier,
164 | ground.color[2]
165 | ),
166 | color(stemHue, (s - 10) * saturationMultiplier, l + lightness),
167 | i / maxLetters + 0.5
168 | );
169 | gr.stroke(stemColor);
170 |
171 | gr.strokeWeight(w);
172 | gr.line(0, 0, segmentLength, 0);
173 | gr.translate(segmentLength, 0);
174 | } else if (c === "+") {
175 | gr.rotate(angle);
176 | } else if (c === "-") {
177 | gr.rotate(-angle);
178 | } else if (c === "[") {
179 | w--;
180 | gr.push();
181 | } else if (c === "]") {
182 | w++;
183 | gr.pop();
184 | }
185 |
186 | if (w < 3) {
187 | const rand = randomWidths[i];
188 | gr.push();
189 | gr.noStroke();
190 | if (Math.floor(rand) === 10) {
191 | gr.fill(fruitHue, 50, 50 + lightness, 0.9);
192 | gr.ellipse(-2, 0, rand, rand);
193 | } else {
194 | gr.push();
195 | // gr.colorMode(RGB);
196 | gr.fill(leafHue, 40 * saturationMultiplier, 40 + lightness, 0.7);
197 | if (parseInt(rand) % 2) {
198 | gr.translate(0, -rand / 2);
199 | gr.rotate(-10 + noiseVal / 100);
200 | gr.ellipse(0, 0, rand, rand * 0.6);
201 | } else {
202 | gr.translate(0, rand / 2);
203 | gr.rotate(10 + noiseVal / 100);
204 | gr.ellipse(0, 0, rand, rand * 0.6);
205 | }
206 | gr.pop();
207 | }
208 | gr.pop();
209 | }
210 | }
211 |
212 | if (
213 | isGrowing &&
214 | frameCount % framesPerStep === 0 &&
215 | maxLetters < sentence.length
216 | ) {
217 | // console.log("incrementing");
218 | maxLetters += growSpeed;
219 | if (maxLetters >= sentence.length) {
220 | isDoneGrowing = true;
221 | }
222 | }
223 |
224 | image(gr, 0, 0, width, height);
225 | tint(255, 100);
226 |
227 | this.count++;
228 | };
229 | }
230 |
--------------------------------------------------------------------------------
/sketch.js:
--------------------------------------------------------------------------------
1 | const plants = [];
2 | const waterDrops = [];
3 |
4 | let c; // the canvas
5 |
6 | const btns = [];
7 |
8 | const clearBtns = () => {
9 | btns.forEach((btn) => {
10 | btn.removeClass("isActive");
11 | });
12 | };
13 |
14 | let waterDelta = 0;
15 | let groundTriangles;
16 | let activeSeedType = "CIRCLE";
17 | let activeTool = "draw";
18 | let backgroundGraphics;
19 |
20 | // function so that width and height are instantiated
21 | const getGroundTriangles = () => [
22 | {
23 | ground: new Ground1([0, 10, 25]),
24 | points: [
25 | { x: 0, y: height },
26 | { x: width / 3, y: height },
27 | { x: 0, y: 0 },
28 | ],
29 | },
30 | {
31 | ground: new Ground3([200, 10, 25]),
32 | points: [
33 | { x: 0, y: 0 },
34 | { x: width, y: 0 },
35 | { x: width / 3, y: height },
36 | ],
37 | },
38 | {
39 | ground: new Ground2([40, 10, 25]),
40 | points: [
41 | { x: width, y: height },
42 | { x: width, y: 0 },
43 | { x: width / 3, y: height },
44 | ],
45 | },
46 | {
47 | ground: new Ground4([0, 54, 25]),
48 | points: [
49 | { x: width * 0.2, y: height },
50 | { x: width * 0.7, y: height },
51 | { x: width * 0.4, y: height * 0.8 },
52 | ],
53 | },
54 | {
55 | ground: new Ground5([0, 0, 0]),
56 | points: [
57 | { x: width * 0.4, y: height * 0.3 },
58 | { x: width * 1, y: 0 },
59 | { x: width * 0.5, y: height * 0.6 },
60 | ],
61 | },
62 | ];
63 |
64 | const wateringNoise = new Tone.Noise("pink").chain(
65 | new Tone.Gain(0.15),
66 | OUTPUT_NODE
67 | );
68 |
69 | // const COLORS = { CIRCLE: [50, 80, 20], SQUARE: [200, 110, 10] };
70 | const COLORS = {
71 | CIRCLE: [80, 50, 50],
72 | SQUARE: [10, 50, 50],
73 | TRIANGLE: [50, 60, 50],
74 | SAWTOOTH: [30, 50, 50],
75 | WATER: [190, 30, 65],
76 | };
77 |
78 | const plantTypes = {
79 | CIRCLE: {
80 | label: "circle",
81 | waveform: "sine",
82 | baseColor: COLORS.CIRCLE,
83 | ruleset: {
84 | startingLetter: "F",
85 | rules: [
86 | [
87 | "F",
88 | [
89 | { value: "FF+[F-G+]-[-GF+F]", weight: 5 },
90 | { value: "F-F+", weight: 1 },
91 | { value: "F+G-", weight: 1 },
92 | ],
93 | ],
94 | ["G", [{ value: "GG", weight: 1 }]],
95 | ],
96 | nGenerations: 4,
97 | segmentLength: 8,
98 | },
99 | getInitialTheta: () => random(5, 20),
100 | drawSeed: (x, y, ctx_) => {
101 | const ctx = ctx_ || window;
102 | ctx.colorMode(HSL);
103 | ctx.stroke([...COLORS.CIRCLE]);
104 | ctx.strokeWeight(2);
105 | ctx.ellipse(x, y, 20, 20);
106 | },
107 | },
108 | SQUARE: {
109 | label: "square",
110 | waveform: "square",
111 | baseColor: COLORS.SQUARE,
112 | ruleset: {
113 | startingLetter: "G",
114 | rules: [
115 | [
116 | "G",
117 | [
118 | { value: "F+[-G]--F[+G]", weight: 1 },
119 | { value: "F-[+G]++F[-G]", weight: 1 },
120 | ],
121 | ],
122 | [
123 | "F",
124 | [
125 | { value: "FF", weight: 10 },
126 | { value: "F-F+", weight: 1 },
127 | ],
128 | ],
129 | ],
130 | nGenerations: 4,
131 | segmentLength: 19,
132 | },
133 | getInitialTheta: () => 90,
134 | drawSeed: (x, y, ctx_) => {
135 | const ctx = ctx_ || window;
136 | ctx.colorMode(HSL);
137 | ctx.stroke([...COLORS.SQUARE]);
138 | ctx.strokeWeight(2);
139 | ctx.rectMode(CENTER);
140 | ctx.rect(x, y, 20, 20);
141 | },
142 | },
143 | TRIANGLE: {
144 | label: "triangle",
145 | waveform: "triangle",
146 | baseColor: COLORS.TRIANGLE,
147 | ruleset: {
148 | startingLetter: "F",
149 | rules: [
150 | [
151 | "F",
152 | [
153 | { value: "FF-[-F+F+F]+[+F-F-F]", weight: 9 },
154 | { value: "FF", weight: 2 },
155 | { value: "FFF", weight: 1 },
156 | ],
157 | ],
158 | ],
159 | nGenerations: 3,
160 | segmentLength: 15,
161 | },
162 | getInitialTheta: () => 30,
163 | drawSeed: (x, y, ctx_) => {
164 | const ctx = ctx_ || window;
165 | ctx.colorMode(HSL);
166 | ctx.beginShape();
167 | ctx.stroke([...COLORS.TRIANGLE]);
168 | ctx.strokeWeight(2);
169 | ctx.vertex(x, y - 13);
170 | ctx.vertex(x - 13, y + 10);
171 | ctx.vertex(x + 13, y + 10);
172 | ctx.endShape(CLOSE);
173 | },
174 | },
175 | SAWTOOTH: {
176 | label: "sawtooth",
177 | waveform: "sawtooth",
178 | baseColor: COLORS.SAWTOOTH,
179 | ruleset: {
180 | startingLetter: "F",
181 | rules: [
182 | [
183 | "F",
184 | [
185 | { value: "F[+F]F[-F]F", weight: 9 },
186 | { value: "FF[+F]", weight: 1 },
187 | { value: "FF[-F]", weight: 1 },
188 | ],
189 | ],
190 | ],
191 | nGenerations: 4,
192 | segmentLength: 12,
193 | },
194 | getInitialTheta: () => random(25, 35),
195 | drawSeed: (x, y, ctx_) => {
196 | const ctx = ctx_ || window;
197 | ctx.colorMode(HSL);
198 | ctx.beginShape();
199 | ctx.stroke([...COLORS.SAWTOOTH]);
200 | ctx.strokeWeight(2);
201 | ctx.vertex(x - 18, y - 10);
202 | ctx.vertex(x - 18, y + 10);
203 | ctx.vertex(x + 18, y + 10);
204 | ctx.endShape(CLOSE);
205 | },
206 | },
207 | };
208 |
209 | const setSeedType = (type) => {
210 | activeSeedType = type;
211 | };
212 |
213 | const initButtons = () => {
214 | Object.keys(plantTypes).forEach((key, i) => {
215 | const { baseColor, label, drawSeed } = plantTypes[key];
216 | const btn = createButton("");
217 | btn.position(i * 50 + 10, 10);
218 | btn.addClass("btn");
219 | btns.push(btn);
220 | const [h, s, l] = baseColor;
221 | // btn.addClass(`btn--${label}`);
222 | if (activeSeedType === key) {
223 | btn.addClass(`isActive`);
224 | }
225 | btn.mousePressed(() => {
226 | activeSeedType = key;
227 | activeTool = "draw";
228 | clearBtns();
229 | btn.addClass(`isActive`);
230 | });
231 | btn.style(`border-color: hsla(${h}, ${s}%, ${l}%, 1)`);
232 | btn.style(`color: hsla(${h}, ${s}%, ${l}%, 1)`);
233 | btn.html(`${i + 1} `);
234 | });
235 |
236 | const waterBtn = createButton("");
237 | waterBtn.addClass("btn");
238 | waterBtn.addClass("btn--water");
239 | waterBtn.html(
240 | '5 '
241 | );
242 | waterBtn.position(230, 10);
243 | waterBtn.mousePressed(() => {
244 | clearBtns();
245 | activeTool = "water";
246 | waterBtn.addClass(`isActive`);
247 | });
248 | const [h, s, l] = COLORS.WATER;
249 | waterBtn.style(
250 | `color: hsl(${h}, ${s}%, ${l}%); border-color: hsl(${h}, ${s}%, ${l}%); `
251 | );
252 |
253 | btns.push(waterBtn);
254 | };
255 |
256 | const drawBackground = () => {
257 | const randomBackgroundPoints = [...new Array(1000)].map(() => ({
258 | x: parseInt(random(0, width)),
259 | y: parseInt(random(0, height)),
260 | }));
261 |
262 | backgroundGraphics.colorMode(HSL);
263 | backgroundGraphics.background(0, 50, 50);
264 | groundTriangles.forEach(({ points, ground }) => {
265 | const [h, s, l] = ground.color;
266 | backgroundGraphics.beginShape();
267 | backgroundGraphics.noStroke();
268 | backgroundGraphics.fill(h, s, l);
269 |
270 | points.forEach(({ x, y }) => {
271 | backgroundGraphics.vertex(x, y);
272 | });
273 |
274 | backgroundGraphics.endShape(CLOSE);
275 | });
276 |
277 | randomBackgroundPoints.forEach((p) => {
278 | for (let i = 0; i < groundTriangles.length; i++) {
279 | const { points, ground } = groundTriangles[i];
280 | if (pointInTriangle(...points)(p)) {
281 | // colorMode(HSL);
282 | backgroundGraphics.push();
283 | ground.renderGround(p, backgroundGraphics);
284 | backgroundGraphics.pop();
285 | }
286 | }
287 | });
288 | };
289 |
290 | const getGroundTriangleForPoint = (p) => {
291 | for (let i = groundTriangles.length - 1; i >= 0; i--) {
292 | const { points } = groundTriangles[i];
293 | if (pointInTriangle(...points)(p)) {
294 | return groundTriangles[i];
295 | }
296 | }
297 | console.error("Point is not in any area", p);
298 | };
299 |
300 | const drawWater = () => {
301 | noStroke();
302 | if (mouseIsPressed) {
303 | for (let i = 0; i < 5; i++) {
304 | for (let j = 0; j < Math.min(waterDelta, 20); j++) {
305 | if (waterDelta > j) {
306 | fill(...COLORS.WATER, noise(i, j));
307 | const x = random(mouseX - 25, mouseX + 25);
308 | const y = mouseY + ((waterDelta * 2 + j * 12) % 200);
309 | ellipse(x, y, 2, 7);
310 | }
311 | }
312 | }
313 | waterDelta++;
314 | }
315 | };
316 |
317 | const drawButtons = () => {
318 | Object.values(plantTypes).forEach(({ baseColor, label, drawSeed }, i) => {
319 | push();
320 | colorMode(HSL);
321 | fill(...baseColor, 0.8);
322 | stroke(...baseColor);
323 | strokeWeight(2);
324 | rectMode(CENTER);
325 | ellipseMode(CENTER);
326 | translate(i * 50 + 40, 40);
327 |
328 | if (label === "circle") {
329 | scale(1.1);
330 | }
331 | if (label === "triangle") {
332 | scale(0.9);
333 | }
334 | if (label === "sawtooth") {
335 | scale(0.8);
336 | }
337 |
338 | drawSeed(0, 0);
339 | pop();
340 | });
341 | };
342 |
343 | function setup() {
344 | pixelDensity(1);
345 | c = createCanvas(window.innerWidth, window.innerHeight);
346 | c.mousePressed(canvasMousePressed);
347 | groundTriangles = getGroundTriangles();
348 | backgroundGraphics = createGraphics(width, height);
349 | drawBackground();
350 | initButtons();
351 | textFont("Roboto Mono");
352 | }
353 |
354 | function draw() {
355 | // background(15);
356 | image(backgroundGraphics, 0, 0, width, height);
357 |
358 | plants
359 | // .sort((a, b) => a.startPos.y - b.startPos.y)
360 | .forEach((plant) => {
361 | push();
362 | plant.render();
363 | pop();
364 | });
365 |
366 | drawButtons();
367 |
368 | if (activeTool === "draw" && mouseIsInDrawArea()) {
369 | fill(...COLORS[activeSeedType]);
370 | noStroke();
371 | textAlign(CENTER);
372 | text("Click to plant", mouseX, mouseY - 25);
373 | }
374 |
375 | if (activeTool === "water" && mouseIsInDrawArea()) {
376 | fill(...COLORS.WATER);
377 | noStroke();
378 | text("Hold to water", mouseX, mouseY - 25);
379 | }
380 |
381 | if (!mouseIsInDrawArea()) {
382 | return;
383 | }
384 |
385 | if (activeTool === "draw") {
386 | fill(...COLORS[activeSeedType], 0.3);
387 | plantTypes[activeSeedType].drawSeed(mouseX, mouseY);
388 | }
389 |
390 | if (activeTool === "water") {
391 | noStroke();
392 | fill(...COLORS.WATER, 130);
393 | ellipse(mouseX, mouseY, 50, 10);
394 | drawWater();
395 | }
396 | }
397 |
398 | function mouseReleased() {
399 | wateringNoise.volume.linearRampTo(-Infinity, 0.4);
400 | setTimeout(() => {
401 | wateringNoise.stop();
402 | }, 400);
403 | }
404 |
405 | const canvasMousePressed = () => {
406 | if (!mouseIsInDrawArea()) return;
407 |
408 | waterDelta = 0;
409 |
410 | if (activeTool === "draw") {
411 | const startPos = { x: mouseX, y: mouseY };
412 | const { ground } = getGroundTriangleForPoint(startPos);
413 | // console.log(ground);
414 | const {
415 | baseColor,
416 | waveform,
417 | ruleset,
418 | drawSeed,
419 | getInitialTheta,
420 | } = plantTypes[activeSeedType];
421 |
422 | plants.push(
423 | new Plant({
424 | startPos,
425 | waveform,
426 | theta: getInitialTheta(),
427 | baseColor,
428 | ruleset,
429 | drawSeed,
430 | getPart: ground.getPart,
431 | ground: ground,
432 | })
433 | );
434 | }
435 |
436 | if (activeTool === "water") {
437 | wateringNoise.start();
438 | wateringNoise.volume.value = -Infinity;
439 | wateringNoise.volume.linearRampTo(0.3, 0.4);
440 | }
441 | };
442 |
443 | function keyPressed() {
444 | if (key === "1") {
445 | setSeedType("CIRCLE");
446 | clearBtns();
447 | btns[0].addClass("isActive");
448 | activeTool = "draw";
449 | }
450 | if (key === "2") {
451 | setSeedType("SQUARE");
452 | clearBtns();
453 | btns[1].addClass("isActive");
454 | activeTool = "draw";
455 | }
456 | if (key === "3") {
457 | setSeedType("TRIANGLE");
458 | clearBtns();
459 | btns[2].addClass("isActive");
460 | activeTool = "draw";
461 | }
462 | if (key === "4") {
463 | setSeedType("SAWTOOTH");
464 | clearBtns();
465 | btns[3].addClass("isActive");
466 | activeTool = "draw";
467 | }
468 | if (key === "5") {
469 | clearBtns();
470 | btns[4].addClass("isActive");
471 | activeTool = "water";
472 | }
473 | }
474 |
475 | const updateToneListener = () => {
476 | Tone.Listener.positionX.value = mouseX;
477 | Tone.Listener.positionY.value = mouseY;
478 | };
479 |
480 | function mouseMoved() {
481 | updateToneListener();
482 | }
483 |
484 | function mouseDragged() {
485 | updateToneListener();
486 | }
487 |
--------------------------------------------------------------------------------
/teoria.js:
--------------------------------------------------------------------------------
1 | (function (f) {
2 | if (typeof exports === "object" && typeof module !== "undefined") {
3 | module.exports = f();
4 | } else if (typeof define === "function" && define.amd) {
5 | define([], f);
6 | } else {
7 | var g;
8 | if (typeof window !== "undefined") {
9 | g = window;
10 | } else if (typeof global !== "undefined") {
11 | g = global;
12 | } else if (typeof self !== "undefined") {
13 | g = self;
14 | } else {
15 | g = this;
16 | }
17 | g.teoria = f();
18 | }
19 | })(function () {
20 | var define, module, exports;
21 | return (function e(t, n, r) {
22 | function s(o, u) {
23 | if (!n[o]) {
24 | if (!t[o]) {
25 | var a = typeof require == "function" && require;
26 | if (!u && a) return a(o, !0);
27 | if (i) return i(o, !0);
28 | var f = new Error("Cannot find module '" + o + "'");
29 | throw ((f.code = "MODULE_NOT_FOUND"), f);
30 | }
31 | var l = (n[o] = { exports: {} });
32 | t[o][0].call(
33 | l.exports,
34 | function (e) {
35 | var n = t[o][1][e];
36 | return s(n ? n : e);
37 | },
38 | l,
39 | l.exports,
40 | e,
41 | t,
42 | n,
43 | r
44 | );
45 | }
46 | return n[o].exports;
47 | }
48 | var i = typeof require == "function" && require;
49 | for (var o = 0; o < r.length; o++) s(r[o]);
50 | return s;
51 | })(
52 | {
53 | 1: [
54 | function (require, module, exports) {
55 | var Note = require("./lib/note");
56 | var Interval = require("./lib/interval");
57 | var Chord = require("./lib/chord");
58 | var Scale = require("./lib/scale");
59 |
60 | var teoria;
61 |
62 | // never thought I would write this, but: Legacy support
63 | function intervalConstructor(from, to) {
64 | // Construct a Interval object from string representation
65 | if (typeof from === "string") return Interval.toCoord(from);
66 |
67 | if (typeof to === "string" && from instanceof Note)
68 | return Interval.from(from, Interval.toCoord(to));
69 |
70 | if (to instanceof Interval && from instanceof Note)
71 | return Interval.from(from, to);
72 |
73 | if (to instanceof Note && from instanceof Note)
74 | return Interval.between(from, to);
75 |
76 | throw new Error("Invalid parameters");
77 | }
78 |
79 | intervalConstructor.toCoord = Interval.toCoord;
80 | intervalConstructor.from = Interval.from;
81 | intervalConstructor.between = Interval.between;
82 | intervalConstructor.invert = Interval.invert;
83 |
84 | function noteConstructor(name, duration) {
85 | if (typeof name === "string")
86 | return Note.fromString(name, duration);
87 | else return new Note(name, duration);
88 | }
89 |
90 | noteConstructor.fromString = Note.fromString;
91 | noteConstructor.fromKey = Note.fromKey;
92 | noteConstructor.fromFrequency = Note.fromFrequency;
93 | noteConstructor.fromMIDI = Note.fromMIDI;
94 |
95 | function chordConstructor(name, symbol) {
96 | if (typeof name === "string") {
97 | var root, octave;
98 | root = name.match(/^([a-h])(x|#|bb|b?)/i);
99 | if (root && root[0]) {
100 | octave = typeof symbol === "number" ? symbol.toString(10) : "4";
101 | return new Chord(
102 | Note.fromString(root[0].toLowerCase() + octave),
103 | name.substr(root[0].length)
104 | );
105 | }
106 | } else if (name instanceof Note) return new Chord(name, symbol);
107 |
108 | throw new Error("Invalid Chord. Couldn't find note name");
109 | }
110 |
111 | function scaleConstructor(tonic, scale) {
112 | tonic = tonic instanceof Note ? tonic : teoria.note(tonic);
113 | return new Scale(tonic, scale);
114 | }
115 |
116 | teoria = {
117 | note: noteConstructor,
118 |
119 | chord: chordConstructor,
120 |
121 | interval: intervalConstructor,
122 |
123 | scale: scaleConstructor,
124 |
125 | Note: Note,
126 | Chord: Chord,
127 | Scale: Scale,
128 | Interval: Interval,
129 | };
130 |
131 | require("./lib/sugar")(teoria);
132 | exports = module.exports = teoria;
133 | },
134 | {
135 | "./lib/chord": 2,
136 | "./lib/interval": 3,
137 | "./lib/note": 5,
138 | "./lib/scale": 6,
139 | "./lib/sugar": 7,
140 | },
141 | ],
142 | 2: [
143 | function (require, module, exports) {
144 | var daccord = require("daccord");
145 | var knowledge = require("./knowledge");
146 | var Note = require("./note");
147 | var Interval = require("./interval");
148 |
149 | function Chord(root, name) {
150 | if (!(this instanceof Chord)) return new Chord(root, name);
151 | name = name || "";
152 | this.name = root.name().toUpperCase() + root.accidental() + name;
153 | this.symbol = name;
154 | this.root = root;
155 | this.intervals = [];
156 | this._voicing = [];
157 |
158 | var bass = name.split("/");
159 | if (bass.length === 2 && bass[1].trim() !== "9") {
160 | name = bass[0];
161 | bass = bass[1].trim();
162 | } else {
163 | bass = null;
164 | }
165 |
166 | this.intervals = daccord(name).map(Interval.toCoord);
167 | this._voicing = this.intervals.slice();
168 |
169 | if (bass) {
170 | var intervals = this.intervals,
171 | bassInterval,
172 | note;
173 | // Make sure the bass is atop of the root note
174 | note = Note.fromString(bass + (root.octave() + 1)); // crude
175 |
176 | bassInterval = Interval.between(root, note);
177 | bass = bassInterval.simple();
178 | bassInterval = bassInterval.invert().direction("down");
179 |
180 | this._voicing = [bassInterval];
181 | for (var i = 0, length = intervals.length; i < length; i++) {
182 | if (!intervals[i].simple().equal(bass))
183 | this._voicing.push(intervals[i]);
184 | }
185 | }
186 | }
187 |
188 | Chord.prototype = {
189 | notes: function () {
190 | var root = this.root;
191 | return this.voicing().map(function (interval) {
192 | return root.interval(interval);
193 | });
194 | },
195 |
196 | simple: function () {
197 | return this.notes().map(function (n) {
198 | return n.toString(true);
199 | });
200 | },
201 |
202 | bass: function () {
203 | return this.root.interval(this._voicing[0]);
204 | },
205 |
206 | voicing: function (voicing) {
207 | // Get the voicing
208 | if (!voicing) {
209 | return this._voicing;
210 | }
211 |
212 | // Set the voicing
213 | this._voicing = [];
214 | for (var i = 0, length = voicing.length; i < length; i++) {
215 | this._voicing[i] = Interval.toCoord(voicing[i]);
216 | }
217 |
218 | return this;
219 | },
220 |
221 | resetVoicing: function () {
222 | this._voicing = this.intervals;
223 | },
224 |
225 | dominant: function (additional) {
226 | additional = additional || "";
227 | return new Chord(this.root.interval("P5"), additional);
228 | },
229 |
230 | subdominant: function (additional) {
231 | additional = additional || "";
232 | return new Chord(this.root.interval("P4"), additional);
233 | },
234 |
235 | parallel: function (additional) {
236 | additional = additional || "";
237 | var quality = this.quality();
238 |
239 | if (
240 | this.chordType() !== "triad" ||
241 | quality === "diminished" ||
242 | quality === "augmented"
243 | ) {
244 | throw new Error("Only major/minor triads have parallel chords");
245 | }
246 |
247 | if (quality === "major") {
248 | return new Chord(this.root.interval("m3", "down"), "m");
249 | } else {
250 | return new Chord(this.root.interval("m3", "up"));
251 | }
252 | },
253 |
254 | quality: function () {
255 | var third,
256 | fifth,
257 | seventh,
258 | intervals = this.intervals;
259 |
260 | for (var i = 0, length = intervals.length; i < length; i++) {
261 | if (intervals[i].number() === 3) {
262 | third = intervals[i];
263 | } else if (intervals[i].number() === 5) {
264 | fifth = intervals[i];
265 | } else if (intervals[i].number() === 7) {
266 | seventh = intervals[i];
267 | }
268 | }
269 |
270 | if (!third) {
271 | return;
272 | }
273 |
274 | third = third.direction() === "down" ? third.invert() : third;
275 | third = third.simple().toString();
276 |
277 | if (fifth) {
278 | fifth = fifth.direction === "down" ? fifth.invert() : fifth;
279 | fifth = fifth.simple().toString();
280 | }
281 |
282 | if (seventh) {
283 | seventh =
284 | seventh.direction === "down" ? seventh.invert() : seventh;
285 | seventh = seventh.simple().toString();
286 | }
287 |
288 | if (third === "M3") {
289 | if (fifth === "A5") {
290 | return "augmented";
291 | } else if (fifth === "P5") {
292 | return seventh === "m7" ? "dominant" : "major";
293 | }
294 |
295 | return "major";
296 | } else if (third === "m3") {
297 | if (fifth === "P5") {
298 | return "minor";
299 | } else if (fifth === "d5") {
300 | return seventh === "m7" ? "half-diminished" : "diminished";
301 | }
302 |
303 | return "minor";
304 | }
305 | },
306 |
307 | chordType: function () {
308 | // In need of better name
309 | var length = this.intervals.length,
310 | interval,
311 | has,
312 | invert,
313 | i,
314 | name;
315 |
316 | if (length === 2) {
317 | return "dyad";
318 | } else if (length === 3) {
319 | has = { unison: false, third: false, fifth: false };
320 | for (i = 0; i < length; i++) {
321 | interval = this.intervals[i];
322 | invert = interval.invert();
323 | if (interval.base() in has) {
324 | has[interval.base()] = true;
325 | } else if (invert.base() in has) {
326 | has[invert.base()] = true;
327 | }
328 | }
329 |
330 | name =
331 | has.unison && has.third && has.fifth ? "triad" : "trichord";
332 | } else if (length === 4) {
333 | has = {
334 | unison: false,
335 | third: false,
336 | fifth: false,
337 | seventh: false,
338 | };
339 | for (i = 0; i < length; i++) {
340 | interval = this.intervals[i];
341 | invert = interval.invert();
342 | if (interval.base() in has) {
343 | has[interval.base()] = true;
344 | } else if (invert.base() in has) {
345 | has[invert.base()] = true;
346 | }
347 | }
348 |
349 | if (has.unison && has.third && has.fifth && has.seventh) {
350 | name = "tetrad";
351 | }
352 | }
353 |
354 | return name || "unknown";
355 | },
356 |
357 | get: function (interval) {
358 | if (
359 | typeof interval === "string" &&
360 | interval in knowledge.stepNumber
361 | ) {
362 | var intervals = this.intervals,
363 | i,
364 | length;
365 |
366 | interval = knowledge.stepNumber[interval];
367 | for (i = 0, length = intervals.length; i < length; i++) {
368 | if (intervals[i].number() === interval) {
369 | return this.root.interval(intervals[i]);
370 | }
371 | }
372 |
373 | return null;
374 | } else {
375 | throw new Error("Invalid interval name");
376 | }
377 | },
378 |
379 | interval: function (interval) {
380 | return new Chord(this.root.interval(interval), this.symbol);
381 | },
382 |
383 | transpose: function (interval) {
384 | this.root.transpose(interval);
385 | this.name =
386 | this.root.name().toUpperCase() +
387 | this.root.accidental() +
388 | this.symbol;
389 |
390 | return this;
391 | },
392 |
393 | toString: function () {
394 | return this.name;
395 | },
396 | };
397 |
398 | module.exports = Chord;
399 | },
400 | { "./interval": 3, "./knowledge": 4, "./note": 5, daccord: 10 },
401 | ],
402 | 3: [
403 | function (require, module, exports) {
404 | var knowledge = require("./knowledge");
405 | var vector = require("./vector");
406 | var toCoord = require("interval-coords");
407 |
408 | function Interval(coord) {
409 | if (!(this instanceof Interval)) return new Interval(coord);
410 | this.coord = coord;
411 | }
412 |
413 | Interval.prototype = {
414 | name: function () {
415 | return knowledge.intervalsIndex[this.number() - 1];
416 | },
417 |
418 | semitones: function () {
419 | return vector.sum(vector.mul(this.coord, [12, 7]));
420 | },
421 |
422 | number: function () {
423 | return Math.abs(this.value());
424 | },
425 |
426 | value: function () {
427 | var toMultiply = Math.floor((this.coord[1] - 2) / 7) + 1;
428 | var product = vector.mul(knowledge.sharp, toMultiply);
429 | var without = vector.sub(this.coord, product);
430 | var i = knowledge.intervalFromFifth[without[1] + 5];
431 | var diff = without[0] - knowledge.intervals[i][0];
432 | var val = knowledge.stepNumber[i] + diff * 7;
433 |
434 | return val > 0 ? val : val - 2;
435 | },
436 |
437 | type: function () {
438 | return knowledge.intervals[this.base()][0] <= 1
439 | ? "perfect"
440 | : "minor";
441 | },
442 |
443 | base: function () {
444 | var product = vector.mul(knowledge.sharp, this.qualityValue());
445 | var fifth = vector.sub(this.coord, product)[1];
446 | fifth = this.value() > 0 ? fifth + 5 : -(fifth - 5) % 7;
447 | fifth =
448 | fifth < 0 ? knowledge.intervalFromFifth.length + fifth : fifth;
449 |
450 | var name = knowledge.intervalFromFifth[fifth];
451 | if (name === "unison" && this.number() >= 8) name = "octave";
452 |
453 | return name;
454 | },
455 |
456 | direction: function (dir) {
457 | if (dir) {
458 | var is = this.value() >= 1 ? "up" : "down";
459 | if (is !== dir) this.coord = vector.mul(this.coord, -1);
460 |
461 | return this;
462 | } else return this.value() >= 1 ? "up" : "down";
463 | },
464 |
465 | simple: function (ignore) {
466 | // Get the (upwards) base interval (with quality)
467 | var simple = knowledge.intervals[this.base()];
468 | var toAdd = vector.mul(knowledge.sharp, this.qualityValue());
469 | simple = vector.add(simple, toAdd);
470 |
471 | // Turn it around if necessary
472 | if (!ignore)
473 | simple =
474 | this.direction() === "down" ? vector.mul(simple, -1) : simple;
475 |
476 | return new Interval(simple);
477 | },
478 |
479 | isCompound: function () {
480 | return this.number() > 8;
481 | },
482 |
483 | octaves: function () {
484 | var toSubtract, without, octaves;
485 |
486 | if (this.direction() === "up") {
487 | toSubtract = vector.mul(knowledge.sharp, this.qualityValue());
488 | without = vector.sub(this.coord, toSubtract);
489 | octaves = without[0] - knowledge.intervals[this.base()][0];
490 | } else {
491 | toSubtract = vector.mul(knowledge.sharp, -this.qualityValue());
492 | without = vector.sub(this.coord, toSubtract);
493 | octaves = -(without[0] + knowledge.intervals[this.base()][0]);
494 | }
495 |
496 | return octaves;
497 | },
498 |
499 | invert: function () {
500 | var i = this.base();
501 | var qual = this.qualityValue();
502 | var acc = this.type() === "minor" ? -(qual - 1) : -qual;
503 | var idx = 9 - knowledge.stepNumber[i] - 1;
504 | var coord = knowledge.intervals[knowledge.intervalsIndex[idx]];
505 | coord = vector.add(coord, vector.mul(knowledge.sharp, acc));
506 |
507 | return new Interval(coord);
508 | },
509 |
510 | quality: function (lng) {
511 | var quality =
512 | knowledge.alterations[this.type()][this.qualityValue() + 2];
513 |
514 | return lng ? knowledge.qualityLong[quality] : quality;
515 | },
516 |
517 | qualityValue: function () {
518 | if (this.direction() === "down")
519 | return Math.floor((-this.coord[1] - 2) / 7) + 1;
520 | else return Math.floor((this.coord[1] - 2) / 7) + 1;
521 | },
522 |
523 | equal: function (interval) {
524 | return (
525 | this.coord[0] === interval.coord[0] &&
526 | this.coord[1] === interval.coord[1]
527 | );
528 | },
529 |
530 | greater: function (interval) {
531 | var semi = this.semitones();
532 | var isemi = interval.semitones();
533 |
534 | // If equal in absolute size, measure which interval is bigger
535 | // For example P4 is bigger than A3
536 | return semi === isemi
537 | ? this.number() > interval.number()
538 | : semi > isemi;
539 | },
540 |
541 | smaller: function (interval) {
542 | return !this.equal(interval) && !this.greater(interval);
543 | },
544 |
545 | add: function (interval) {
546 | return new Interval(vector.add(this.coord, interval.coord));
547 | },
548 |
549 | toString: function (ignore) {
550 | // If given true, return the positive value
551 | var number = ignore ? this.number() : this.value();
552 |
553 | return this.quality() + number;
554 | },
555 | };
556 |
557 | Interval.toCoord = function (simple) {
558 | var coord = toCoord(simple);
559 | if (!coord) throw new Error("Invalid simple format interval");
560 |
561 | return new Interval(coord);
562 | };
563 |
564 | Interval.from = function (from, to) {
565 | return from.interval(to);
566 | };
567 |
568 | Interval.between = function (from, to) {
569 | return new Interval(vector.sub(to.coord, from.coord));
570 | };
571 |
572 | Interval.invert = function (sInterval) {
573 | return Interval.toCoord(sInterval).invert().toString();
574 | };
575 |
576 | module.exports = Interval;
577 | },
578 | { "./knowledge": 4, "./vector": 8, "interval-coords": 12 },
579 | ],
580 | 4: [
581 | function (require, module, exports) {
582 | // Note coordinates [octave, fifth] relative to C
583 | module.exports = {
584 | notes: {
585 | c: [0, 0],
586 | d: [-1, 2],
587 | e: [-2, 4],
588 | f: [1, -1],
589 | g: [0, 1],
590 | a: [-1, 3],
591 | b: [-2, 5],
592 | h: [-2, 5],
593 | },
594 |
595 | intervals: {
596 | unison: [0, 0],
597 | second: [3, -5],
598 | third: [2, -3],
599 | fourth: [1, -1],
600 | fifth: [0, 1],
601 | sixth: [3, -4],
602 | seventh: [2, -2],
603 | octave: [1, 0],
604 | },
605 |
606 | intervalFromFifth: [
607 | "second",
608 | "sixth",
609 | "third",
610 | "seventh",
611 | "fourth",
612 | "unison",
613 | "fifth",
614 | ],
615 |
616 | intervalsIndex: [
617 | "unison",
618 | "second",
619 | "third",
620 | "fourth",
621 | "fifth",
622 | "sixth",
623 | "seventh",
624 | "octave",
625 | "ninth",
626 | "tenth",
627 | "eleventh",
628 | "twelfth",
629 | "thirteenth",
630 | "fourteenth",
631 | "fifteenth",
632 | ],
633 |
634 | // linear index to fifth = (2 * index + 1) % 7
635 | fifths: ["f", "c", "g", "d", "a", "e", "b"],
636 | accidentals: ["bb", "b", "", "#", "x"],
637 |
638 | sharp: [-4, 7],
639 | A4: [3, 3],
640 |
641 | durations: {
642 | 0.25: "longa",
643 | 0.5: "breve",
644 | 1: "whole",
645 | 2: "half",
646 | 4: "quarter",
647 | 8: "eighth",
648 | 16: "sixteenth",
649 | 32: "thirty-second",
650 | 64: "sixty-fourth",
651 | 128: "hundred-twenty-eighth",
652 | },
653 |
654 | qualityLong: {
655 | P: "perfect",
656 | M: "major",
657 | m: "minor",
658 | A: "augmented",
659 | AA: "doubly augmented",
660 | d: "diminished",
661 | dd: "doubly diminished",
662 | },
663 |
664 | alterations: {
665 | perfect: ["dd", "d", "P", "A", "AA"],
666 | minor: ["dd", "d", "m", "M", "A", "AA"],
667 | },
668 |
669 | symbols: {
670 | min: ["m3", "P5"],
671 | m: ["m3", "P5"],
672 | "-": ["m3", "P5"],
673 |
674 | M: ["M3", "P5"],
675 | "": ["M3", "P5"],
676 |
677 | "+": ["M3", "A5"],
678 | aug: ["M3", "A5"],
679 |
680 | dim: ["m3", "d5"],
681 | o: ["m3", "d5"],
682 |
683 | maj: ["M3", "P5", "M7"],
684 | dom: ["M3", "P5", "m7"],
685 | ø: ["m3", "d5", "m7"],
686 |
687 | 5: ["P5"],
688 | },
689 |
690 | chordShort: {
691 | major: "M",
692 | minor: "m",
693 | augmented: "aug",
694 | diminished: "dim",
695 | "half-diminished": "7b5",
696 | power: "5",
697 | dominant: "7",
698 | },
699 |
700 | stepNumber: {
701 | unison: 1,
702 | first: 1,
703 | second: 2,
704 | third: 3,
705 | fourth: 4,
706 | fifth: 5,
707 | sixth: 6,
708 | seventh: 7,
709 | octave: 8,
710 | ninth: 9,
711 | eleventh: 11,
712 | thirteenth: 13,
713 | },
714 |
715 | // Adjusted Shearer syllables - Chromatic solfege system
716 | // Some intervals are not provided for. These include:
717 | // dd2 - Doubly diminished second
718 | // dd3 - Doubly diminished third
719 | // AA3 - Doubly augmented third
720 | // dd6 - Doubly diminished sixth
721 | // dd7 - Doubly diminished seventh
722 | // AA7 - Doubly augmented seventh
723 | intervalSolfege: {
724 | dd1: "daw",
725 | d1: "de",
726 | P1: "do",
727 | A1: "di",
728 | AA1: "dai",
729 | d2: "raw",
730 | m2: "ra",
731 | M2: "re",
732 | A2: "ri",
733 | AA2: "rai",
734 | d3: "maw",
735 | m3: "me",
736 | M3: "mi",
737 | A3: "mai",
738 | dd4: "faw",
739 | d4: "fe",
740 | P4: "fa",
741 | A4: "fi",
742 | AA4: "fai",
743 | dd5: "saw",
744 | d5: "se",
745 | P5: "so",
746 | A5: "si",
747 | AA5: "sai",
748 | d6: "law",
749 | m6: "le",
750 | M6: "la",
751 | A6: "li",
752 | AA6: "lai",
753 | d7: "taw",
754 | m7: "te",
755 | M7: "ti",
756 | A7: "tai",
757 | dd8: "daw",
758 | d8: "de",
759 | P8: "do",
760 | A8: "di",
761 | AA8: "dai",
762 | },
763 | };
764 | },
765 | {},
766 | ],
767 | 5: [
768 | function (require, module, exports) {
769 | var scientific = require("scientific-notation");
770 | var helmholtz = require("helmholtz");
771 | var pitchFq = require("pitch-fq");
772 | var knowledge = require("./knowledge");
773 | var vector = require("./vector");
774 | var Interval = require("./interval");
775 |
776 | function pad(str, ch, len) {
777 | for (; len > 0; len--) {
778 | str += ch;
779 | }
780 |
781 | return str;
782 | }
783 |
784 | function Note(coord, duration) {
785 | if (!(this instanceof Note)) return new Note(coord, duration);
786 | duration = duration || {};
787 |
788 | this.duration = {
789 | value: duration.value || 4,
790 | dots: duration.dots || 0,
791 | };
792 | this.coord = coord;
793 | }
794 |
795 | Note.prototype = {
796 | octave: function () {
797 | return (
798 | this.coord[0] +
799 | knowledge.A4[0] -
800 | knowledge.notes[this.name()][0] +
801 | this.accidentalValue() * 4
802 | );
803 | },
804 |
805 | name: function () {
806 | var value = this.accidentalValue();
807 | var idx = this.coord[1] + knowledge.A4[1] - value * 7 + 1;
808 | return knowledge.fifths[idx];
809 | },
810 |
811 | accidentalValue: function () {
812 | return Math.round((this.coord[1] + knowledge.A4[1] - 2) / 7);
813 | },
814 |
815 | accidental: function () {
816 | return knowledge.accidentals[this.accidentalValue() + 2];
817 | },
818 |
819 | /**
820 | * Returns the key number of the note
821 | */
822 | key: function (white) {
823 | if (white) return this.coord[0] * 7 + this.coord[1] * 4 + 29;
824 | else return this.coord[0] * 12 + this.coord[1] * 7 + 49;
825 | },
826 |
827 | /**
828 | * Returns a number ranging from 0-127 representing a MIDI note value
829 | */
830 | midi: function () {
831 | return this.key() + 20;
832 | },
833 |
834 | /**
835 | * Calculates and returns the frequency of the note.
836 | * Optional concert pitch (def. 440)
837 | */
838 | fq: function (concertPitch) {
839 | return pitchFq(this.coord, concertPitch);
840 | },
841 |
842 | /**
843 | * Returns the pitch class index (chroma) of the note
844 | */
845 | chroma: function () {
846 | var value =
847 | (vector.sum(vector.mul(this.coord, [12, 7])) - 3) % 12;
848 |
849 | return value < 0 ? value + 12 : value;
850 | },
851 |
852 | interval: function (interval) {
853 | if (typeof interval === "string")
854 | interval = Interval.toCoord(interval);
855 |
856 | if (interval instanceof Interval)
857 | return new Note(
858 | vector.add(this.coord, interval.coord),
859 | this.duration
860 | );
861 | else if (interval instanceof Note)
862 | return new Interval(vector.sub(interval.coord, this.coord));
863 | },
864 |
865 | transpose: function (interval) {
866 | this.coord = vector.add(this.coord, interval.coord);
867 | return this;
868 | },
869 |
870 | /**
871 | * Returns the Helmholtz notation form of the note (fx C,, d' F# g#'')
872 | */
873 | helmholtz: function () {
874 | var octave = this.octave();
875 | var name = this.name();
876 | name = octave < 3 ? name.toUpperCase() : name.toLowerCase();
877 | var padchar = octave < 3 ? "," : "'";
878 | var padcount = octave < 2 ? 2 - octave : octave - 3;
879 |
880 | return pad(name + this.accidental(), padchar, padcount);
881 | },
882 |
883 | /**
884 | * Returns the scientific notation form of the note (fx E4, Bb3, C#7 etc.)
885 | */
886 | scientific: function () {
887 | return (
888 | this.name().toUpperCase() + this.accidental() + this.octave()
889 | );
890 | },
891 |
892 | /**
893 | * Returns notes that are enharmonic with this note.
894 | */
895 | enharmonics: function (oneaccidental) {
896 | var key = this.key(),
897 | limit = oneaccidental ? 2 : 3;
898 |
899 | return ["m3", "m2", "m-2", "m-3"]
900 | .map(this.interval.bind(this))
901 | .filter(function (note) {
902 | var acc = note.accidentalValue();
903 | var diff = key - (note.key() - acc);
904 |
905 | if (diff < limit && diff > -limit) {
906 | var product = vector.mul(knowledge.sharp, diff - acc);
907 | note.coord = vector.add(note.coord, product);
908 | return true;
909 | }
910 | });
911 | },
912 |
913 | solfege: function (scale, showOctaves) {
914 | var interval = scale.tonic.interval(this),
915 | solfege,
916 | stroke,
917 | count;
918 | if (interval.direction() === "down") interval = interval.invert();
919 |
920 | if (showOctaves) {
921 | count = (this.key(true) - scale.tonic.key(true)) / 7;
922 | count = count >= 0 ? Math.floor(count) : -Math.ceil(-count);
923 | stroke = count >= 0 ? "'" : ",";
924 | }
925 |
926 | solfege =
927 | knowledge.intervalSolfege[interval.simple(true).toString()];
928 | return showOctaves
929 | ? pad(solfege, stroke, Math.abs(count))
930 | : solfege;
931 | },
932 |
933 | scaleDegree: function (scale) {
934 | var inter = scale.tonic.interval(this);
935 |
936 | // If the direction is down, or we're dealing with an octave - invert it
937 | if (
938 | inter.direction() === "down" ||
939 | (inter.coord[1] === 0 && inter.coord[0] !== 0)
940 | ) {
941 | inter = inter.invert();
942 | }
943 |
944 | inter = inter.simple(true).coord;
945 |
946 | return scale.scale.reduce(function (index, current, i) {
947 | var coord = Interval.toCoord(current).coord;
948 | return coord[0] === inter[0] && coord[1] === inter[1]
949 | ? i + 1
950 | : index;
951 | }, 0);
952 | },
953 |
954 | /**
955 | * Returns the name of the duration value,
956 | * such as 'whole', 'quarter', 'sixteenth' etc.
957 | */
958 | durationName: function () {
959 | return knowledge.durations[this.duration.value];
960 | },
961 |
962 | /**
963 | * Returns the duration of the note (including dots)
964 | * in seconds. The first argument is the tempo in beats
965 | * per minute, the second is the beat unit (i.e. the
966 | * lower numeral in a time signature).
967 | */
968 | durationInSeconds: function (bpm, beatUnit) {
969 | var secs = 60 / bpm / (this.duration.value / 4) / (beatUnit / 4);
970 | return secs * 2 - secs / Math.pow(2, this.duration.dots);
971 | },
972 |
973 | /**
974 | * Returns the name of the note, with an optional display of octave number
975 | */
976 | toString: function (dont) {
977 | return (
978 | this.name() + this.accidental() + (dont ? "" : this.octave())
979 | );
980 | },
981 | };
982 |
983 | Note.fromString = function (name, dur) {
984 | var coord = scientific(name);
985 | if (!coord) coord = helmholtz(name);
986 | return new Note(coord, dur);
987 | };
988 |
989 | Note.fromKey = function (key) {
990 | var octave = Math.floor((key - 4) / 12);
991 | var distance = key - octave * 12 - 4;
992 | var name = knowledge.fifths[(2 * Math.round(distance / 2) + 1) % 7];
993 | var subDiff = vector.sub(knowledge.notes[name], knowledge.A4);
994 | var note = vector.add(subDiff, [octave + 1, 0]);
995 | var diff = key - 49 - vector.sum(vector.mul(note, [12, 7]));
996 |
997 | var arg = diff
998 | ? vector.add(note, vector.mul(knowledge.sharp, diff))
999 | : note;
1000 | return new Note(arg);
1001 | };
1002 |
1003 | Note.fromFrequency = function (fq, concertPitch) {
1004 | var key, cents, originalFq;
1005 | concertPitch = concertPitch || 440;
1006 |
1007 | key =
1008 | 49 + 12 * ((Math.log(fq) - Math.log(concertPitch)) / Math.log(2));
1009 | key = Math.round(key);
1010 | originalFq = concertPitch * Math.pow(2, (key - 49) / 12);
1011 | cents = 1200 * (Math.log(fq / originalFq) / Math.log(2));
1012 |
1013 | return { note: Note.fromKey(key), cents: cents };
1014 | };
1015 |
1016 | Note.fromMIDI = function (note) {
1017 | return Note.fromKey(note - 20);
1018 | };
1019 |
1020 | module.exports = Note;
1021 | },
1022 | {
1023 | "./interval": 3,
1024 | "./knowledge": 4,
1025 | "./vector": 8,
1026 | helmholtz: 11,
1027 | "pitch-fq": 14,
1028 | "scientific-notation": 15,
1029 | },
1030 | ],
1031 | 6: [
1032 | function (require, module, exports) {
1033 | var knowledge = require("./knowledge");
1034 | var Interval = require("./interval");
1035 |
1036 | var scales = {
1037 | aeolian: ["P1", "M2", "m3", "P4", "P5", "m6", "m7"],
1038 | blues: ["P1", "m3", "P4", "d5", "P5", "m7"],
1039 | chromatic: [
1040 | "P1",
1041 | "m2",
1042 | "M2",
1043 | "m3",
1044 | "M3",
1045 | "P4",
1046 | "A4",
1047 | "P5",
1048 | "m6",
1049 | "M6",
1050 | "m7",
1051 | "M7",
1052 | ],
1053 | dorian: ["P1", "M2", "m3", "P4", "P5", "M6", "m7"],
1054 | doubleharmonic: ["P1", "m2", "M3", "P4", "P5", "m6", "M7"],
1055 | harmonicminor: ["P1", "M2", "m3", "P4", "P5", "m6", "M7"],
1056 | ionian: ["P1", "M2", "M3", "P4", "P5", "M6", "M7"],
1057 | locrian: ["P1", "m2", "m3", "P4", "d5", "m6", "m7"],
1058 | lydian: ["P1", "M2", "M3", "A4", "P5", "M6", "M7"],
1059 | majorpentatonic: ["P1", "M2", "M3", "P5", "M6"],
1060 | melodicminor: ["P1", "M2", "m3", "P4", "P5", "M6", "M7"],
1061 | minorpentatonic: ["P1", "m3", "P4", "P5", "m7"],
1062 | mixolydian: ["P1", "M2", "M3", "P4", "P5", "M6", "m7"],
1063 | phrygian: ["P1", "m2", "m3", "P4", "P5", "m6", "m7"],
1064 | wholetone: ["P1", "M2", "M3", "A4", "A5", "A6"],
1065 | };
1066 |
1067 | // synonyms
1068 | scales.harmonicchromatic = scales.chromatic;
1069 | scales.minor = scales.aeolian;
1070 | scales.major = scales.ionian;
1071 | scales.flamenco = scales.doubleharmonic;
1072 |
1073 | function Scale(tonic, scale) {
1074 | if (!(this instanceof Scale)) return new Scale(tonic, scale);
1075 | var scaleName, i;
1076 | if (!("coord" in tonic)) {
1077 | throw new Error("Invalid Tonic");
1078 | }
1079 |
1080 | if (typeof scale === "string") {
1081 | scaleName = scale;
1082 | scale = scales[scale];
1083 | if (!scale) throw new Error("Invalid Scale");
1084 | } else {
1085 | for (i in scales) {
1086 | if (scales.hasOwnProperty(i)) {
1087 | if (scales[i].toString() === scale.toString()) {
1088 | scaleName = i;
1089 | break;
1090 | }
1091 | }
1092 | }
1093 | }
1094 |
1095 | this.name = scaleName;
1096 | this.tonic = tonic;
1097 | this.scale = scale;
1098 | }
1099 |
1100 | Scale.prototype = {
1101 | notes: function () {
1102 | var notes = [];
1103 |
1104 | for (var i = 0, length = this.scale.length; i < length; i++) {
1105 | notes.push(this.tonic.interval(this.scale[i]));
1106 | }
1107 |
1108 | return notes;
1109 | },
1110 |
1111 | simple: function () {
1112 | return this.notes().map(function (n) {
1113 | return n.toString(true);
1114 | });
1115 | },
1116 |
1117 | type: function () {
1118 | var length = this.scale.length - 2;
1119 | if (length < 8) {
1120 | return (
1121 | ["di", "tri", "tetra", "penta", "hexa", "hepta", "octa"][
1122 | length
1123 | ] + "tonic"
1124 | );
1125 | }
1126 | },
1127 |
1128 | get: function (i) {
1129 | var isStepStr =
1130 | typeof i === "string" && i in knowledge.stepNumber;
1131 | i = isStepStr ? knowledge.stepNumber[i] : i;
1132 | var len = this.scale.length;
1133 | var interval, octaves;
1134 |
1135 | if (i < 0) {
1136 | interval = this.scale[(i % len) + len - 1];
1137 | octaves = Math.floor((i - 1) / len);
1138 | } else if (i % len === 0) {
1139 | interval = this.scale[len - 1];
1140 | octaves = i / len - 1;
1141 | } else {
1142 | interval = this.scale[(i % len) - 1];
1143 | octaves = Math.floor(i / len);
1144 | }
1145 |
1146 | return this.tonic
1147 | .interval(interval)
1148 | .interval(new Interval([octaves, 0]));
1149 | },
1150 |
1151 | solfege: function (index, showOctaves) {
1152 | if (index) return this.get(index).solfege(this, showOctaves);
1153 |
1154 | return this.notes().map(function (n) {
1155 | return n.solfege(this, showOctaves);
1156 | });
1157 | },
1158 |
1159 | interval: function (interval) {
1160 | interval =
1161 | typeof interval === "string"
1162 | ? Interval.toCoord(interval)
1163 | : interval;
1164 | return new Scale(this.tonic.interval(interval), this.scale);
1165 | },
1166 |
1167 | transpose: function (interval) {
1168 | var scale = this.interval(interval);
1169 | this.scale = scale.scale;
1170 | this.tonic = scale.tonic;
1171 |
1172 | return this;
1173 | },
1174 | };
1175 | Scale.KNOWN_SCALES = Object.keys(scales);
1176 |
1177 | module.exports = Scale;
1178 | },
1179 | { "./interval": 3, "./knowledge": 4 },
1180 | ],
1181 | 7: [
1182 | function (require, module, exports) {
1183 | var knowledge = require("./knowledge");
1184 |
1185 | module.exports = function (teoria) {
1186 | var Note = teoria.Note;
1187 | var Chord = teoria.Chord;
1188 | var Scale = teoria.Scale;
1189 |
1190 | Note.prototype.chord = function (chord) {
1191 | var isShortChord = chord in knowledge.chordShort;
1192 | chord = isShortChord ? knowledge.chordShort[chord] : chord;
1193 |
1194 | return new Chord(this, chord);
1195 | };
1196 |
1197 | Note.prototype.scale = function (scale) {
1198 | return new Scale(this, scale);
1199 | };
1200 | };
1201 | },
1202 | { "./knowledge": 4 },
1203 | ],
1204 | 8: [
1205 | function (require, module, exports) {
1206 | module.exports = {
1207 | add: function (note, interval) {
1208 | return [note[0] + interval[0], note[1] + interval[1]];
1209 | },
1210 |
1211 | sub: function (note, interval) {
1212 | return [note[0] - interval[0], note[1] - interval[1]];
1213 | },
1214 |
1215 | mul: function (note, interval) {
1216 | if (typeof interval === "number")
1217 | return [note[0] * interval, note[1] * interval];
1218 | else return [note[0] * interval[0], note[1] * interval[1]];
1219 | },
1220 |
1221 | sum: function (coord) {
1222 | return coord[0] + coord[1];
1223 | },
1224 | };
1225 | },
1226 | {},
1227 | ],
1228 | 9: [
1229 | function (require, module, exports) {
1230 | var accidentalValues = {
1231 | bb: -2,
1232 | b: -1,
1233 | "": 0,
1234 | "#": 1,
1235 | x: 2,
1236 | };
1237 |
1238 | module.exports = function accidentalNumber(acc) {
1239 | return accidentalValues[acc];
1240 | };
1241 |
1242 | module.exports.interval = function accidentalInterval(acc) {
1243 | var val = accidentalValues[acc];
1244 | return [-4 * val, 7 * val];
1245 | };
1246 | },
1247 | {},
1248 | ],
1249 | 10: [
1250 | function (require, module, exports) {
1251 | var SYMBOLS = {
1252 | m: ["m3", "P5"],
1253 | mi: ["m3", "P5"],
1254 | min: ["m3", "P5"],
1255 | "-": ["m3", "P5"],
1256 |
1257 | M: ["M3", "P5"],
1258 | ma: ["M3", "P5"],
1259 | "": ["M3", "P5"],
1260 |
1261 | "+": ["M3", "A5"],
1262 | aug: ["M3", "A5"],
1263 |
1264 | dim: ["m3", "d5"],
1265 | o: ["m3", "d5"],
1266 |
1267 | maj: ["M3", "P5", "M7"],
1268 | dom: ["M3", "P5", "m7"],
1269 | ø: ["m3", "d5", "m7"],
1270 |
1271 | 5: ["P5"],
1272 |
1273 | "6/9": ["M3", "P5", "M6", "M9"],
1274 | };
1275 |
1276 | module.exports = function (symbol) {
1277 | var c,
1278 | parsing = "quality",
1279 | additionals = [],
1280 | name,
1281 | chordLength = 2;
1282 | var notes = ["P1", "M3", "P5", "m7", "M9", "P11", "M13"];
1283 | var explicitMajor = false;
1284 |
1285 | function setChord(name) {
1286 | var intervals = SYMBOLS[name];
1287 | for (var i = 0, len = intervals.length; i < len; i++) {
1288 | notes[i + 1] = intervals[i];
1289 | }
1290 |
1291 | chordLength = intervals.length;
1292 | }
1293 |
1294 | // Remove whitespace, commas and parentheses
1295 | symbol = symbol.replace(/[,\s\(\)]/g, "");
1296 | for (var i = 0, len = symbol.length; i < len; i++) {
1297 | if (!(c = symbol[i])) return;
1298 |
1299 | if (parsing === "quality") {
1300 | var sub3 =
1301 | i + 2 < len ? symbol.substr(i, 3).toLowerCase() : null;
1302 | var sub2 =
1303 | i + 1 < len ? symbol.substr(i, 2).toLowerCase() : null;
1304 | if (sub3 in SYMBOLS) name = sub3;
1305 | else if (sub2 in SYMBOLS) name = sub2;
1306 | else if (c in SYMBOLS) name = c;
1307 | else name = "";
1308 |
1309 | if (name) setChord(name);
1310 |
1311 | if (name === "M" || name === "ma" || name === "maj")
1312 | explicitMajor = true;
1313 |
1314 | i += name.length - 1;
1315 | parsing = "extension";
1316 | } else if (parsing === "extension") {
1317 | c = c === "1" && symbol[i + 1] ? +symbol.substr(i, 2) : +c;
1318 |
1319 | if (!isNaN(c) && c !== 6) {
1320 | chordLength = (c - 1) / 2;
1321 |
1322 | if (chordLength !== Math.round(chordLength))
1323 | return new Error(
1324 | "Invalid interval extension: " + c.toString(10)
1325 | );
1326 |
1327 | if (name === "o" || name === "dim") notes[3] = "d7";
1328 | else if (explicitMajor) notes[3] = "M7";
1329 |
1330 | i += c >= 10 ? 1 : 0;
1331 | } else if (c === 6) {
1332 | notes[3] = "M6";
1333 | chordLength = Math.max(3, chordLength);
1334 | } else i -= 1;
1335 |
1336 | parsing = "alterations";
1337 | } else if (parsing === "alterations") {
1338 | var alterations = symbol
1339 | .substr(i)
1340 | .split(/(#|b|add|maj|sus|M)/i),
1341 | next,
1342 | flat = false,
1343 | sharp = false;
1344 |
1345 | if (alterations.length === 1)
1346 | return new Error("Invalid alteration");
1347 | else if (alterations[0].length !== 0)
1348 | return new Error("Invalid token: '" + alterations[0] + "'");
1349 |
1350 | var ignore = false;
1351 | alterations.forEach(function (alt, i, arr) {
1352 | if (ignore || !alt.length) return (ignore = false);
1353 |
1354 | var next = arr[i + 1],
1355 | lower = alt.toLowerCase();
1356 | if (alt === "M" || lower === "maj") {
1357 | if (next === "7") ignore = true;
1358 |
1359 | chordLength = Math.max(3, chordLength);
1360 | notes[3] = "M7";
1361 | } else if (lower === "sus") {
1362 | var type = "P4";
1363 | if (next === "2" || next === "4") {
1364 | ignore = true;
1365 |
1366 | if (next === "2") type = "M2";
1367 | }
1368 |
1369 | notes[1] = type; // Replace third with M2 or P4
1370 | } else if (lower === "add") {
1371 | if (next === "9") additionals.push("M9");
1372 | else if (next === "11") additionals.push("P11");
1373 | else if (next === "13") additionals.push("M13");
1374 |
1375 | ignore = true;
1376 | } else if (lower === "b") {
1377 | flat = true;
1378 | } else if (lower === "#") {
1379 | sharp = true;
1380 | } else {
1381 | var token = +alt,
1382 | quality,
1383 | intPos;
1384 | if (isNaN(token) || String(token).length !== alt.length)
1385 | return new Error("Invalid token: '" + alt + "'");
1386 |
1387 | if (token === 6) {
1388 | if (sharp) notes[3] = "A6";
1389 | else if (flat) notes[3] = "m6";
1390 | else notes[3] = "M6";
1391 |
1392 | chordLength = Math.max(3, chordLength);
1393 | return;
1394 | }
1395 |
1396 | // Calculate the position in the 'note' array
1397 | intPos = (token - 1) / 2;
1398 | if (chordLength < intPos) chordLength = intPos;
1399 |
1400 | if (
1401 | token < 5 ||
1402 | token === 7 ||
1403 | intPos !== Math.round(intPos)
1404 | )
1405 | return new Error("Invalid interval alteration: " + token);
1406 |
1407 | quality = notes[intPos][0];
1408 |
1409 | // Alterate the quality of the interval according the accidentals
1410 | if (sharp) {
1411 | if (quality === "d") quality = "m";
1412 | else if (quality === "m") quality = "M";
1413 | else if (quality === "M" || quality === "P")
1414 | quality = "A";
1415 | } else if (flat) {
1416 | if (quality === "A") quality = "M";
1417 | else if (quality === "M") quality = "m";
1418 | else if (quality === "m" || quality === "P")
1419 | quality = "d";
1420 | }
1421 |
1422 | sharp = flat = false;
1423 | notes[intPos] = quality + token;
1424 | }
1425 | });
1426 | parsing = "ended";
1427 | } else if (parsing === "ended") {
1428 | break;
1429 | }
1430 | }
1431 |
1432 | return notes.slice(0, chordLength + 1).concat(additionals);
1433 | };
1434 | },
1435 | {},
1436 | ],
1437 | 11: [
1438 | function (require, module, exports) {
1439 | var coords = require("notecoord");
1440 | var accval = require("accidental-value");
1441 |
1442 | module.exports = function helmholtz(name) {
1443 | var name = name.replace(/\u2032/g, "'").replace(/\u0375/g, ",");
1444 | var parts = name.match(/^(,*)([a-h])(x|#|bb|b?)([,\']*)$/i);
1445 |
1446 | if (!parts || name !== parts[0])
1447 | throw new Error("Invalid formatting");
1448 |
1449 | var note = parts[2];
1450 | var octaveFirst = parts[1];
1451 | var octaveLast = parts[4];
1452 | var lower = note === note.toLowerCase();
1453 | var octave;
1454 |
1455 | if (octaveFirst) {
1456 | if (lower)
1457 | throw new Error(
1458 | "Invalid formatting - found commas before lowercase note"
1459 | );
1460 |
1461 | octave = 2 - octaveFirst.length;
1462 | } else if (octaveLast) {
1463 | if (octaveLast.match(/^'+$/) && lower)
1464 | octave = 3 + octaveLast.length;
1465 | else if (octaveLast.match(/^,+$/) && !lower)
1466 | octave = 2 - octaveLast.length;
1467 | else
1468 | throw new Error(
1469 | "Invalid formatting - mismatch between octave " +
1470 | "indicator and letter case"
1471 | );
1472 | } else octave = lower ? 3 : 2;
1473 |
1474 | var accidentalValue = accval.interval(parts[3].toLowerCase());
1475 | var coord = coords(note.toLowerCase());
1476 |
1477 | coord[0] += octave;
1478 | coord[0] += accidentalValue[0] - coords.A4[0];
1479 | coord[1] += accidentalValue[1] - coords.A4[1];
1480 |
1481 | return coord;
1482 | };
1483 | },
1484 | { "accidental-value": 9, notecoord: 13 },
1485 | ],
1486 | 12: [
1487 | function (require, module, exports) {
1488 | var pattern = /^(AA|A|P|M|m|d|dd)(-?\d+)$/;
1489 |
1490 | // The interval it takes to raise a note a semitone
1491 | var sharp = [-4, 7];
1492 |
1493 | var pAlts = ["dd", "d", "P", "A", "AA"];
1494 | var mAlts = ["dd", "d", "m", "M", "A", "AA"];
1495 |
1496 | var baseIntervals = [
1497 | [0, 0],
1498 | [3, -5],
1499 | [2, -3],
1500 | [1, -1],
1501 | [0, 1],
1502 | [3, -4],
1503 | [2, -2],
1504 | [1, 0],
1505 | ];
1506 |
1507 | module.exports = function (simple) {
1508 | var parser = simple.match(pattern);
1509 | if (!parser) return null;
1510 |
1511 | var quality = parser[1];
1512 | var number = +parser[2];
1513 | var sign = number < 0 ? -1 : 1;
1514 |
1515 | number = sign < 0 ? -number : number;
1516 |
1517 | var lower = number > 8 ? number % 7 || 7 : number;
1518 | var octaves = (number - lower) / 7;
1519 |
1520 | var base = baseIntervals[lower - 1];
1521 | var alts = base[0] <= 1 ? pAlts : mAlts;
1522 | var alt = alts.indexOf(quality) - 2;
1523 |
1524 | // this happens, if the alteration wasn't suitable for this type
1525 | // of interval, such as P2 or M5 (no "perfect second" or "major fifth")
1526 | if (alt === -3) return null;
1527 |
1528 | return [
1529 | sign * (base[0] + octaves + sharp[0] * alt),
1530 | sign * (base[1] + sharp[1] * alt),
1531 | ];
1532 | };
1533 |
1534 | // Copy to avoid overwriting internal base intervals
1535 | module.exports.coords = baseIntervals.slice(0);
1536 | },
1537 | {},
1538 | ],
1539 | 13: [
1540 | function (require, module, exports) {
1541 | // First coord is octaves, second is fifths. Distances are relative to c
1542 | var notes = {
1543 | c: [0, 0],
1544 | d: [-1, 2],
1545 | e: [-2, 4],
1546 | f: [1, -1],
1547 | g: [0, 1],
1548 | a: [-1, 3],
1549 | b: [-2, 5],
1550 | h: [-2, 5],
1551 | };
1552 |
1553 | module.exports = function (name) {
1554 | return name in notes ? [notes[name][0], notes[name][1]] : null;
1555 | };
1556 |
1557 | module.exports.notes = notes;
1558 | module.exports.A4 = [3, 3]; // Relative to C0 (scientic notation, ~16.35Hz)
1559 | module.exports.sharp = [-4, 7];
1560 | },
1561 | {},
1562 | ],
1563 | 14: [
1564 | function (require, module, exports) {
1565 | module.exports = function (coord, stdPitch) {
1566 | if (typeof coord === "number") {
1567 | stdPitch = coord;
1568 | return function (coord) {
1569 | return (
1570 | stdPitch * Math.pow(2, (coord[0] * 12 + coord[1] * 7) / 12)
1571 | );
1572 | };
1573 | }
1574 |
1575 | stdPitch = stdPitch || 440;
1576 | return stdPitch * Math.pow(2, (coord[0] * 12 + coord[1] * 7) / 12);
1577 | };
1578 | },
1579 | {},
1580 | ],
1581 | 15: [
1582 | function (require, module, exports) {
1583 | var coords = require("notecoord");
1584 | var accval = require("accidental-value");
1585 |
1586 | module.exports = function scientific(name) {
1587 | var format = /^([a-h])(x|#|bb|b?)(-?\d*)/i;
1588 |
1589 | var parser = name.match(format);
1590 | if (!(parser && name === parser[0] && parser[3].length)) return;
1591 |
1592 | var noteName = parser[1];
1593 | var octave = +parser[3];
1594 | var accidental = parser[2].length ? parser[2].toLowerCase() : "";
1595 |
1596 | var accidentalValue = accval.interval(accidental);
1597 | var coord = coords(noteName.toLowerCase());
1598 |
1599 | coord[0] += octave;
1600 | coord[0] += accidentalValue[0] - coords.A4[0];
1601 | coord[1] += accidentalValue[1] - coords.A4[1];
1602 |
1603 | return coord;
1604 | };
1605 | },
1606 | { "accidental-value": 9, notecoord: 13 },
1607 | ],
1608 | },
1609 | {},
1610 | [1]
1611 | )(1);
1612 | });
1613 |
--------------------------------------------------------------------------------