From bb62ec75d48df19f146a8a5909c3c14d0dcd0538 Mon Sep 17 00:00:00 2001 From: Mikayla Dobson <93477693+innocuous-symmetry@users.noreply.github.com> Date: Mon, 9 May 2022 16:14:39 -0500 Subject: [PATCH] program can randomly generate chords, removing any duplicate members --- app.css | 123 ++++++++++++++++++++++++++++--- app.js | 70 ++++++++++++++++++ index.html | 169 +++++++++++++++++++++++++------------------ js/app.js | 84 --------------------- js/inputHandling.js | 17 ++++- js/styleUtils.js | 40 ++++++++++ js/toneGeneration.js | 26 +++++++ package.json | 2 +- 8 files changed, 363 insertions(+), 168 deletions(-) create mode 100644 app.js delete mode 100644 js/app.js create mode 100644 js/styleUtils.js diff --git a/app.css b/app.css index 808246a..ea05169 100644 --- a/app.css +++ b/app.css @@ -2,6 +2,32 @@ * { margin: 0; font-family: sans-serif; + font-weight: lighter; +} + +html { + color: white; + background-color: rgb(11, 8, 20); +} + +button { + border: 1px solid white; + color: inherit; + background-color: inherit; + border-radius: 25rem; + padding: 0.2rem 0.7rem; +} + +button:active { + background-color: darkgreen; +} + +#start-tone { + width: 12vw; + height: 5vh; + position: absolute; + left: 45vw; + top: 50%; } /* javascript utilities */ @@ -9,36 +35,115 @@ display: none; } -/* document styles */ +/* header styles */ header { display: block; - position: fixed; + position: sticky; + background-color: black; + border-bottom: 1px solid white; + max-height: 25vh; width: 100%; text-align: center; - min-height: 12rem; - top: 0; padding: 2rem; + top: 0; + z-index: 9; } +header h2 { + margin: 1rem 1rem 1.5rem; +} + +header button { + margin-top: 0.75rem; +} + +header p { + padding: 0.4rem; +} + +#navbar-tools { + display: flex; +} + +#info-button { + position: absolute; + text-align: center; + top: 1rem; + left: 1.5rem; +} + +/* main section style */ main { display: flex; flex-direction: column; width: 100%; + margin-top: 4rem; align-items: center; position: relative; - top: 15rem; + z-index: 8; } -.audio-controls { - display: flex; - width: 80%; +/* styling for animated visuals */ +.visuals { + display: inline-flex; + flex-direction: row; + justify-content: space-between; + height: 18rem; +} + +.voice { + display: inline-block; + background-color: rgb(5, 5, 75); + width: 20vw; + height: 100%; + margin: 0 1rem; +} + +#visuals-border { + display: block; + width: 85vw; + height: 1rem; + background-color: white; + margin: 3rem 0; + border-radius: 15rem; +} + +/* styling for synth control panel */ +.control-row { + display: inline-flex; + flex-direction: row; + align-items: center; + margin: 1rem 0; +} + +.button-row button { + margin: 0 0.4rem; } .controls { display: inline-flex; flex-direction: column; - border: 1px solid black; align-items: center; + justify-content: center; + border: 1px solid white; + width: 18vw; + height: 25vh; + margin: 1rem; +} + +.controls h3 { + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.2em; +} + +.controls label { + display: inline-flex; + flex-direction: row; +} + +.controls label, input { + margin: 0.2rem 0; } #synth-button { diff --git a/app.js b/app.js new file mode 100644 index 0000000..096770a --- /dev/null +++ b/app.js @@ -0,0 +1,70 @@ +import { sopranoTones, altoTones, tenorTones, bassTones, evaluateHarmony } from "./js/toneGeneration.js"; + +const pitchsets = [sopranoTones, altoTones, tenorTones, bassTones]; + +// initialize four synth voices +const soprano = new Tone.Synth().toDestination(); +const alto = new Tone.Synth().toDestination(); +const tenor = new Tone.Synth().toDestination(); +const bass = new Tone.Synth().toDestination(); + +// test function for audio is armed +export const audioTest = () => { + soprano.triggerAttackRelease("C5", "16n"); + alto.triggerAttackRelease("F4", "16n"); + tenor.triggerAttackRelease("E4", "16n"); + bass.triggerAttackRelease("G3", "16n"); +} + +// allows a chord to be generated with input from another function +export const soundChord = (pitches) => { + const [s,a,t,b] = pitches; + soprano.triggerAttackRelease(s, "8n"); + alto.triggerAttackRelease(a, "8n"); + tenor.triggerAttackRelease(t, "8n"); + bass.triggerAttackRelease(b, "8n"); +} + +// initial test: generate a single, random chord +export const fullRandomChord = () => { + let pitches = []; + for (let voice of pitchsets) { + // finds a random index, excluding any which may already exist in the array + let index; + + do { + index = Math.floor(Math.random() * 100) % voice.length; + } while (pitches.includes(voice[index])); + + pitches.push(voice[index]); + console.log(voice[index]); + } + + for (let i = 0; i < pitches.length; i++) { + if (pitches[i] === pitches[i+1]) { + console.log("CAUGHT"); + } + } + + soundChord(pitches); + evaluateHarmony(pitches); +} + +// set up transport +let clock = 0; +let slowClock = 0; + +const transportStart = document.getElementById('transport-start'); + +let transport; + +const loop = new Tone.Loop((time) => { + audioTest(); +}, "8n").start(0); + +loop.probability = 0.8; + +transportStart.onclick = () => { + Tone.Transport.start(); +} + diff --git a/index.html b/index.html index c9e1881..bb9cd11 100644 --- a/index.html +++ b/index.html @@ -6,92 +6,121 @@
-

Procedural Drone, No. 1

+

Procedural Drones, No. 1

Design and engineering by Mikayla Dobson

- +
-

Musical widget designed using Tone.js and the Web Audio API.

+

Music-focused widget designed using Tone.js and the Web Audio API.

Sliders control output parameters of four independent oscillators modulated by four independent low frequency oscillators (LFOs).

JavaScript passes your inputs to the browser and procedurally constructs harmonies based on developer-defined logic based on tenets of music theory.

-
-
-
-
- - + + +
+
+
+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+

Soprano

+ + + +
-
-
-
-
-
+ +
+

Alto

+ + + +
- -
-

Soprano

- - - - -
-
-

Alto

- - - - -
-
-

Tenor

- - - - -
-
-

Bass

- - - - -
- + +
+

Tenor

+ + + + +
+ + +
+

Bass

+ + + + +
+ + +
+
+

Master

+ + +
+
+ +
+ + +
- + + + - + + + \ No newline at end of file diff --git a/js/app.js b/js/app.js deleted file mode 100644 index c1a890c..0000000 --- a/js/app.js +++ /dev/null @@ -1,84 +0,0 @@ -// element identifiers -const startButton = document.getElementById("start-tone"); -const synthButton = document.getElementById("synth-button"); - -const showStart = document.getElementsByClassName('show-on-start'); -const hideStart = document.getElementsByClassName('hide-on-start'); - -const showMore = document.getElementById('info-button'); -const moreInfo = document.getElementsByClassName('more-info'); - -let initialized = false; - -// style utilities -showMore.onclick = () => { - if (showMore.innerHTML === 'Show more info...') { - for (let element of moreInfo) { - element.style.display = 'block'; - document.querySelector('#info-button').innerHTML = "Hide info"; - } - } else if (showMore.innerHTML === 'Hide info') { - for (let element of moreInfo) { - element.style.display = 'none'; - document.querySelector('#info-button').innerHTML = "Show more info..."; - } - } -} - - - -// application start, show synth play button -startButton.onclick = async () => { - await Tone.start() - .then(() => { - synthButton.style.display = "block"; - startButton.style.display = "none"; - - for (let element of showStart) { - element.style.display = "flex"; - } - for (let element of hideStart) { - element.style.display = "none"; - } - }); -} - -// initialize four voices -const soprano = new Tone.Synth().toDestination(); -const alto = new Tone.Synth().toDestination(); -const tenor = new Tone.Synth().toDestination(); -const bass = new Tone.Synth().toDestination(); - -// test -const audioTest = () => { - soprano.triggerAttackRelease("C5", "8n"); - alto.triggerAttackRelease("F4", "8n"); - tenor.triggerAttackRelease("E4", "8n"); - bass.triggerAttackRelease("G3", "8n"); -} - -synthButton.onclick = audioTest; - -// set up transport -let clock = 0; -let slowClock = 0; - -const transportStart = document.getElementById('transport-start'); - -Tone.Transport.schedule((time) => { - clock += 2; - slowClock += 1; - - console.log(`clock: ${clock}, slow: ${slowClock}`); - - if (clock % 4 === 0) { - console.log('caught'); - } - - if (slowClock % 4 === 0) { - console.log("slow"); - audioTest(); - } -}, 1); - -transportStart.onclick = Tone.Transport.start(); diff --git a/js/inputHandling.js b/js/inputHandling.js index f04b3e2..cb4ff7f 100644 --- a/js/inputHandling.js +++ b/js/inputHandling.js @@ -1,3 +1,5 @@ +import { audioTest, fullRandomChord } from '../app.js'; + // slider variables referring to DOM export const sopranoVol = document.getElementById('soprano-vol'); const sopranoVolTarget = document.getElementById('soprano-vol-target'); @@ -26,7 +28,7 @@ sopranoVol.oninput = (e) => { } sopLFO.oninput = (e) => { - sopLFOTarget.innerHTML = e.target.value; + sopLFOTarget.innerHTML = ` ${e.target.value} Hz.`; } // A @@ -35,7 +37,7 @@ altoVol.oninput = (e) => { } altoLFO.oninput = (e) => { - altoLFOTarget.innerHTML = e.target.value; + altoLFOTarget.innerHTML = ` ${e.target.value} Hz.`; } // T @@ -44,7 +46,7 @@ tenVol.oninput = (e) => { } tenLFO.oninput = (e) => { - tenLFOTarget.innerHTML = e.target.value; + tenLFOTarget.innerHTML = ` ${e.target.value} Hz.`; } // B @@ -53,5 +55,12 @@ bassVol.oninput = (e) => { } bassLFO.oninput = (e) => { - bassLFOTarget.innerHTML = e.target.value; + bassLFOTarget.innerHTML = ` ${e.target.value.toString()} Hz.`; } + +// audio-adjacent input handling +const synthButton = document.getElementById('synth-button'); +synthButton.onclick = audioTest; + +const randChord = document.getElementById('rand-chord'); +randChord.onclick = fullRandomChord; diff --git a/js/styleUtils.js b/js/styleUtils.js new file mode 100644 index 0000000..3b7a4a0 --- /dev/null +++ b/js/styleUtils.js @@ -0,0 +1,40 @@ +export const startButton = document.getElementById("start-tone"); +export const synthButton = document.getElementById("synth-button"); + +export const showStart = document.getElementsByClassName('show-on-start'); +export const hideStart = document.getElementsByClassName('hide-on-start'); + +export const showMore = document.getElementById('info-button'); +export const moreInfo = document.getElementsByClassName('more-info'); + +export let appReady = false; + +showMore.onclick = () => { + if (showMore.innerHTML === 'Show more info...') { + for (let element of moreInfo) { + element.style.display = 'block'; + document.querySelector('#info-button').innerHTML = "Hide info"; + } + } else if (showMore.innerHTML === 'Hide info') { + for (let element of moreInfo) { + element.style.display = 'none'; + document.querySelector('#info-button').innerHTML = "Show more info..."; + } + } +} + +startButton.onclick = async () => { + await Tone.start() + .then(() => { + appReady = true; + synthButton.style.display = "block"; + startButton.style.display = "none"; + + for (let element of showStart) { + element.style.display = "flex"; + } + for (let element of hideStart) { + element.style.display = "none"; + } + }); +} diff --git a/js/toneGeneration.js b/js/toneGeneration.js index e69de29..ae78816 100644 --- a/js/toneGeneration.js +++ b/js/toneGeneration.js @@ -0,0 +1,26 @@ +// we start with a selection of pitches that generally work okay together +export const sopranoTones = ["B5", "A5", "G5", "F#5", "F5", "E5", "D5", "C5", "B4", "Bb4", "A4", "G4", "F#4", "F4", "E4"]; +export const altoTones = ["E5", "D5", "C5", "B4", "Bb4", "A4", "G4", "F#4", "F4", "E4", "D4", "C4", "B3", "Bb3", "A3", "G3"]; +export const tenorTones = ["G4", "F#4", "F4", "E4", "D4", "C4", "B3", "Bb3", "A3", "G3", "F3", "E3", "D3", "C3"]; +export const bassTones = ["C2", "D2", "E2", "F2", "G2", "A2", "Bb2", "B2", "C3", "D3", "E3", "F3", "G3"]; + +// now we define some rules to allow for the program to follow so it can some basic tenets of music theory +// we're going to include all pitches, so that it can use semitone-based pitch logic. +// this is focused on base-12, something computers understand quite well +const musicalPitches = ['A', "Bb", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"]; + +export const evaluateHarmony = (pitches) => { + let sorted = []; + for (let each of pitches) { + let matches = each.match(/(\d+)/); + if (matches) { + sorted.push([each, matches]); + } + } + + sorted = sorted.sort((a,b) => a[1] < b[1]); + console.log(sorted); +} + +// no tritones +// no minor 2nds or major 7ths \ No newline at end of file diff --git a/package.json b/package.json index 2efb779..1f3c0e0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tone", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" },