├── _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 | ![Screen Shot 2020-12-09 at 9 07 49 PM](https://user-images.githubusercontent.com/9386882/102028436-4e106800-3d78-11eb-8155-d7e308503980.png) 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 |
20 | Made by 22 | Elias Jarzombek 26 | | 27 | GitHub 30 |
31 | 32 | 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 | --------------------------------------------------------------------------------