Skip to content

Instantly share code, notes, and snippets.

@srghma
Last active January 13, 2023 05:12
Show Gist options
  • Save srghma/3d28ecf2db90edffe66302466c68e5ce to your computer and use it in GitHub Desktop.
Save srghma/3d28ecf2db90edffe66302466c68e5ce to your computer and use it in GitHub Desktop.
import * as React from 'react'
import { useState } from 'react'
import ReactDOM from 'react-dom'
import { StyledEngineProvider } from '@mui/material/styles'
import * as R from 'ramda'
import * as RA from 'ramda-adjunct'
import { useMIDI, useMIDIOutput } from '@react-midi/hooks'
// import { Pcset } from "@tonaljs/tonal"
//
// TODO: calculate Forte number
// TODO: is generated by "Ervin Wilson's Hexany" (not cube https://www.youtube.com/watch?v=-GeR8XbFxvI)
// TODO: perfect vs imperfect scale http://allthescales.org/scales.php?n=6
// What is a scale?
// 1. A scale starts on the root tone.
// 1. A scale does not have any leaps greater than n semitones.
// chroma, valid colors https://github.com/tonaljs/tonal/blob/9622ec8fa4031f0c80515d278dfa06424bf159e5/packages/pcset/index.ts#L161
// does scale have refl symm (palyndrome), IF yes THEN is it chiral
function assert(x, y) {
if (!R.equals(x, y)) {
console.log({ x, y })
throw new Error('assert')
}
}
//////////////////////
const arrRotateLeft1Mutate = a => {
a.push(a.shift())
}
const arrRotateRight1Mutate = a => {
a.unshift(a.pop())
}
const arrRotateLeft = (a, n) => {
const a_ = [...a]
while (n > 0) {
arrRotateLeft1Mutate(a_)
n--
}
return a_
}
const arrRotateRight = (a, n) => {
const a_ = [...a]
while (n > 0) {
arrRotateRight1Mutate(a_)
n--
}
return a_
}
//////////////////////
const binaryToDecimal = x => parseInt(x, 2)
assert(binaryToDecimal("010101010101"), 1365)
assert(binaryToDecimal("101010101010"), 2730)
const decimalToBinary = x => x.toString(2)
//////////////////////
// https://en.wikipedia.org/wiki/Interval_(music)
const numberOfNotesForPerfectUnison = 12
const endingBigEndianBinary = R.times(R.always("1"), numberOfNotesForPerfectUnison).join('')
const endingDecimals = binaryToDecimal(endingBigEndianBinary)
assert(endingDecimals, 4095)
let chords = R.range(1, endingDecimals + 1)
chords = chords.map(decimalToBinary)
chords = chords.map(x => RA.padCharsStart("0", numberOfNotesForPerfectUnison, x))
assert(R.head(chords), "000000000001")
assert(R.last(chords), "111111111111")
const rotateUntil = (predicate, a) => {
const a_ = [...a]
let n = 0
while (predicate(a_[0]) !== true) {
if (n > a.length) {
throw new Error(`rotateUntil: maxTimes for ${JSON.stringify(a)}`)
}
arrRotateLeft1Mutate(a_)
n++
}
if (typeof a === 'string') { return a_.join('') }
return a_
}
assert(rotateUntil(x => x[0] === "1", "010101010101"), "101010101010")
assert(rotateUntil(x => x[0] === "1", "101010101010"), "101010101010")
assert(rotateUntil(x => x[0] === "1", "000101010101"), "101010101000")
function binaryToIndexesOfEnabledNotes(binary) {
// if (binary[0] !== "1" || binary.length !== 12) { throw new Error(binary) }
// console.log(binary)
return binary.split('').map((x, index) => {
// console.log({ x, index })
if (x === "0") { return undefined }
if (x === "1") { return index }
throw new Error('binaryToIndexesOfEnabledNotes')
}).filter(x => x !== undefined)
}
assert(binaryToIndexesOfEnabledNotes("000000000000"), [])
assert(binaryToIndexesOfEnabledNotes("111111111111"), [0,1,2,3,4,5,6,7,8,9,10,11])
assert(binaryToIndexesOfEnabledNotes("010101010101"), [1,3,5,7,9,11])
assert(binaryToIndexesOfEnabledNotes("101010101010"), [0,2,4,6,8,10])
assert(binaryToIndexesOfEnabledNotes("101111111111"), [0,2,3,4,5,6,7,8,9,10,11])
// https://github.com/AtActionPark/Pianissimo/blob/master/lib/theory.js
// https://felixroos.github.io/pitch-class-sets
function indexesOfEnabledNotesToPitchClassSet(indexesOfEnabledNotes) {
const firstNoteIsEnabled = indexesOfEnabledNotes[0] === 0
if (!firstNoteIsEnabled) { throw new Error(indexesOfEnabledNotes) }
// indexesOfEnabledNotes = R.tail(indexesOfEnabledNotes)
const output = indexesOfEnabledNotes.reduce((accumulator, value, index, array) => {
let nextIndexOfEnabledNotes = array[index + 1]
if (nextIndexOfEnabledNotes === undefined) { nextIndexOfEnabledNotes = numberOfNotesForPerfectUnison }
// console.log({ accumulator, value, index, nextIndexOfEnabledNotes })
accumulator.push(nextIndexOfEnabledNotes - value)
return accumulator
}, [])
// console.log(output)
return output
}
assert(indexesOfEnabledNotesToPitchClassSet([0,1,2,3,4,5,6,7,8,9,10,11]), [1,1,1,1,1,1,1,1,1,1,1,1])
assert(indexesOfEnabledNotesToPitchClassSet([0,1,2,3,4,5,6,7,8,9,10,11]), [1,1,1,1,1,1,1,1,1,1,1,1])
assert(indexesOfEnabledNotesToPitchClassSet([0,2,3,4,5,6,7,8,9,10,11]), [2,1,1,1,1,1,1,1,1,1,1])
const permutationCycles = xs => {
const buff = []
for (let index = 0; index < xs.length; index++) {
const buff_ = []
for (let plusIndex = 0; plusIndex < xs.length; plusIndex++) {
const indexSum = plusIndex + index
const index_ = indexSum % xs.length
//console.log({ index, plusIndex, indexSum, index_ })
buff_.push(xs[index_])
}
buff.push(buff_)
}
if (typeof xs === 'string') { buff = buff.map(xs => xs.join('')) }
return R.uniq(buff)
}
assert(permutationCycles([1]), [[1]])
assert(permutationCycles([1,2]), [[1,2], [2,1]])
assert(permutationCycles([1,2,3]), [[1,2,3], [2,3,1], [3,1,2]])
assert(permutationCycles([1,1,1,1,1,1,1,1,1,1,1,1]), [[1,1,1,1,1,1,1,1,1,1,1,1]])
const sortByLengthThenByContent = arr => {
arr = R.groupBy(R.prop('length'), arr)
arr = R.values(arr).map(x => x.sort()).flat()
return arr
}
function normalizedPitchClassSet_rotationalSymmetry(pitchClassSet) {
const scalesThatShareRotationSymmetryWithThisOne = permutationCycles(pitchClassSet)
// console.log({pitchClassSet, scalesThatShareRotationSymmetryWithThisOne})
return scalesThatShareRotationSymmetryWithThisOne.sort()[0]
}
assert(normalizedPitchClassSet_rotationalSymmetry([1,1,1,1,1,1,1,1,1,1,1,1]), [1,1,1,1,1,1,1,1,1,1,1,1])
assert(normalizedPitchClassSet_rotationalSymmetry([2,1,1,1,1,1,1,1,1,1,1]), [1,1,1,1,1,1,1,1,1,1,2])
assert(normalizedPitchClassSet_rotationalSymmetry([12]), [12])
// b.c scalesThatShareRotationSymmetryWithThisOne = [[11, 1], [1, 11]]
assert(normalizedPitchClassSet_rotationalSymmetry(indexesOfEnabledNotesToPitchClassSet(binaryToIndexesOfEnabledNotes("100000000001"))), [1,11])
assert(normalizedPitchClassSet_rotationalSymmetry(indexesOfEnabledNotesToPitchClassSet(binaryToIndexesOfEnabledNotes("110000000000"))), [1,11])
// function normalizedPitchClassSet_transitivity_and_inversion(pitchClassSet) {
// return sortByLengthThenByContent(permutationCycles(pitchClassSet).map(x => [x, x.reverse()]).flat())[0]
// }
// assert(normalizedPitchClassSet_transitivity_and_inversion(binaryToIndexesOfEnabledNotes("100000000010")), [2,10])
// assert(normalizedPitchClassSet_transitivity_and_inversion(binaryToIndexesOfEnabledNotes("101000000000")), [2,10])
function reflection_chord(chord) {
const [first, chord_] = R.splitAt(1, chord)
const [firstHalf, chord__] = R.splitAt(5, chord_)
const [second, secondHalf] = R.splitAt(1, chord__)
const ret = first + R.reverse(secondHalf) + second + R.reverse(firstHalf)
//console.log({chord, chord_, chord__, ret, first, firstHalf, second, secondHalf})
// return first + R.reverse(secondHalf) + second + R.reverse(firstHalf)
return ret
}
assert(reflection_chord("010000000000"), "000000000001")
assert(reflection_chord("100000000001"), "110000000000")
assert(reflection_chord("100000100000"), "100000100000")
assert(reflection_chord("101100100000"), "100000100110")
assert(reflection_chord("001111001111"), "011110011110")
function complement_chord(chord) {
return chord.split('').map(x => x === "1" ? "0" : "1").join('')
}
assert(complement_chord("100000000001"), "011111111110")
assert(complement_chord("110000000000"), "001111111111")
function omitUndefinedFields(obj) {
return Object.keys(obj).reduce((acc, key) => {
const _acc = acc;
if (obj[key] !== undefined) _acc[key] = obj[key];
return _acc;
}, {})
}
const chordToInfo = chord => {
const isScaleAndChord = chord[0] === "1"
const binary = binaryToDecimal(chord)
const indexesOfEnabledNotes = binaryToIndexesOfEnabledNotes(chord)
const commonFields = {
isScaleAndChord,
binary,
indexesOfEnabledNotes,
}
if (isScaleAndChord) {
const pitchClassSet = indexesOfEnabledNotesToPitchClassSet(indexesOfEnabledNotes)
return {
...commonFields,
pitchClassSet,
normalizedPitchClassSet_rotationalSymmetry: normalizedPitchClassSet_rotationalSymmetry(pitchClassSet),
// normalizedPitchClassSet_transitivity_and_inversion: normalizedPitchClassSet_transitivity_and_inversion(pitchClassSet),
// scalesThatShareRotationSymmetryWithThisOne: permutationCycles(pitchClassSet),
}
}
if (chord.includes("1")) {
const approximated_scale = rotateUntil(chord_ => {
const firstNoteIsEnabled = chord_[0] === "1"
return firstNoteIsEnabled
}, chord)
return {
...commonFields,
approximated_scale
}
}
return commonFields
}
chords = chords.map(chord => ({ chord, ...chordToInfo(chord) }))
const chordToInfoObject = R.fromPairs(chords.map(x => [x.chord, x]))
// let scales = chords.filter(x => x.isScaleAndChord)
let scales = chords
// console.log(chordToInfoObject)
scales = R.groupBy(x => x.chord.split('').filter(x => x === "1").length, scales)
scales = R.map(
chords => {
return R.groupBy(
x => {
const info = x.isScaleAndChord ? x : chordToInfoObject[x.approximated_scale]
// console.log(x)
// console.log(info)
return info.normalizedPitchClassSet_rotationalSymmetry
},
chords
)
},
scales
)
// console.log(scales)
// scales = R.map(R.map(
// chords => {
// return R.groupBy(
// x => {
// const info = x.isScaleAndChord ? x : chordToInfoObject[x.approximated_scale]
// // console.log(x)
// // console.log(info)
// return info.normalizedPitchClassSet_rotationalSymmetry
// },
// chords
// )
// }),
// scales
// )
// console.log(scales)
// x = R.uniq(chords.map(([chord, info]) => info.pitchClassSet).filter(x => x))
// x = sortByLengthThenByContent(x)
// console.log(x)
// let x = R.uniq(chords.map(([chord, info]) => info.normalizedPitchClassSet).filter(x => x))
// x = sortByLengthThenByContent(x)
// console.log(x)
wikipediaScaleNames = `Acoustic scale W-W-W-H-W-H-W
1st Messiaen mode W--W--W--W--W--W
2st Messiaen mode W-H--W-H--W-H--W-H
3st Messiaen mode W-H-H--W-H-H--W-H-H
4st Messiaen mode H-H-H-3H--H-H-H-3H
5st Messiaen mode H-4H-H--H-4H-H
6st Messiaen mode W-W-H-H--W-W-H-H
Aeolian mode or natural minor scale W-H-W-W-H-W-W
Algerian scale W-H-3H-H-H-3H-H-W-H-W
Altered scale or Super Locrian scale H-W-H-W-W-W-W
Augmented scale 3H-H-3H-H-3H-H
Bebop dominant scale W-W-H-W-W-H-H-H
Blues scale 3H-W-H-H-3H-W
Chromatic scale H-H-H-H-H-H-H-H-H-H-H-H
Dorian mode W-H-W-W-W-H-W
Double harmonic scale H-3H-H-W-H-3H-H
Enigmatic scale H-3H-W-W-W-H-H
Flamenco mode H-3H-H-W-H-3H-H
"Gypsy" scale W-H-3H-H-H-W-W
Half diminished scale W-H-W-H-W-W-W
Harmonic major scale W-W-H-W-H-3H-H
Harmonic minor scale W-H-W-W-H-3H-H
Hirajoshi scale 2W-W-H-2W-H
Hungarian "Gypsy" scale / Hungarian minor scale W-H-3H-H-H-3H-H
Hungarian major scale 3H-H-W-H-W-H-W
In scale H-2W-W-H-2W
Insen scale H-2W-W-3H-W
Ionian mode or major scale W-W-H-W-W-W-H
Istrian scale H-W-H-W-H-5H
Iwato scale H-2W-H-2W-W
Locrian mode H-W-W-H-W-W-W
Lydian augmented scale W-W-W-W-H-W-H
Lydian mode W-W-W-H-W-W-H
Major bebop scale W-W-H-W-W-W-H
Major Locrian scale W-W-H-H-W-W-W
Major pentatonic scale W-W-3H-W-3H
Melodic minor scale (descending) OR Mixolydian mode or Adonai malakh mode W-W-H-W-W-H-W
Melodic minor scale (ascending) W-H-W-W-W-W-H
Minor pentatonic scale, Yo scale 3H-W-W-3H-W
Neapolitan major scale H-W-W-W-W-W-H
Neapolitan minor scale H-W-W-W-H-3H-H
Octatonic scale W-H-W-H-W-H-W-W-W-H-W-H-W-H
Persian scale H-3H-H-H-W-3H-H
Phrygian dominant scale H-3H-H-W-H-W-W
Phrygian mode H-W-W-W-H-W-W
Prometheus scale W-W-W-3H-H-W
Scale of harmonics 3H-H-H-W-W-3H
Tritone scale H-3H-W-H-3H-W
Two-semitone tritone scale H-H-4H-H-H-4H
Ukrainian Dorian scale W-H-3H-H-W-H-W
Vietnamese scale of harmonics 5Q-Q-H-H-W
Whole tone scale W-W-W-W-W-W`.split('\n').map(x => x.split('\t'))
wikipediaScaleNames = wikipediaScaleNames.map(([name, pattern]) => {
pattern = pattern.split('-').filter(Boolean).map(x => {
let number = 0
if (x.endsWith('Q')) { number = Number(x.replace('Q', '')) * 100 }
if (x.endsWith('H')) { number = Number(x.replace('H', '')) }
if (x.endsWith('W')) { number = Number(x.replace('W', '')) * 2 }
if (x === 'H') { number = 1 }
if (x === 'W') { number = 2 }
if (x === 'Q') { number = 100 }
const valid = number > 0
if (valid) { return number }
throw new Error(JSON.stringify({ name, pattern, x, number }))
})
return { name, pattern }
})
wikipediaScaleNames = R.groupBy(x => x.pattern, wikipediaScaleNames)
wikipediaScaleNames = R.map(x => {
const valid = R.sum(x[0].pattern) === 12
const name = x.map(x => x.name).join(' OR ')
return `${name}${valid ? '' : ' (INVALID)'}`
}, wikipediaScaleNames)
// console.log(wikipediaScaleNames)
function InfoImplementation({ onPlay }) {
const output = R.toPairs(scales).map(([nOfNotes, xs]) => {
xs = R.toPairs(xs).map(([normalizedPitchClassSet_rotationalSymmetry, xs]) => {
// const items = permutationCycles(normalizedPitchClassSet_rotationalSymmetry)
if (R.toPairs(xs).length >= 12) { return "" }
xs = xs.map(x => {
let reflection_chord_ = reflection_chord(x.chord)
let complement_chord_ = complement_chord(x.chord)
let reflected_complement_chord_ = reflection_chord(complement_chord)
if (reflection_chord_ === x.chord) reflection_chord_ = null
if (complement_chord_ === x.chord) complement_chord_ = null
if (reflected_complement_chord_ === x.chord) reflected_complement_chord_ = null
let different = []
if (x.isScaleAndChord) {
const name = wikipediaScaleNames[x.pitchClassSet]
const pitchClassSet_ = x.pitchClassSet.map(x => {
if (x === 1) { return 'H' }
if (x === 2) { return 'W' }
return `${x}H`
}).join('-')
different = <>
<td>{x.pitchClassSet}</td>
<td>{pitchClassSet_}</td>
<td>{name || ''}</td>
</>
} else {
different = <>
<td></td>
<td></td>
<td></td>
</>
}
const onClickHandler = chord => chord ? { onClick: e => onPlay(chord, e.shiftKey) } : {}
// https://drive.google.com/file/d/1WEKn6p2Bh_FOitJvCzo_4yxDPkcMyuc0/view
return <tr key={x.chord}>
<td {...onClickHandler(x.chord)}>{x.chord}</td>
<td><a rel="noopener noreferrer" href={`https://ianring.com/musictheory/scales/${x.binary}`} target="_blank">{x.binary}</a></td>
{different}
<td {...onClickHandler(reflection_chord_)}>{reflection_chord_ || 'palyndrome'}</td>
<td {...onClickHandler(complement_chord_)}>{complement_chord_ || 'same'}</td>
<td {...onClickHandler(reflected_complement_chord_)}>{reflected_complement_chord_ || 'same'}</td>
</tr>
})
return <div key={normalizedPitchClassSet_rotationalSymmetry}>
<h1>rotationalSymmetry: {normalizedPitchClassSet_rotationalSymmetry}, length: {xs.length}</h1>
<table border="1">
<thead>
<tr>
<th>Chord</th>
<th>Binary</th>
<th>Pitch class</th>
<th>Pitch class</th>
<th>Name</th>
<th>Reflection</th>
<th>Complement</th>
<th>Reflected complement</th>
</tr>
</thead>
<tbody>{xs}</tbody>
</table>
</div>
})
return <div key={nOfNotes}>
<h1>N of Notes: {nOfNotes}, length: {xs.length}</h1>
<div>{xs}</div>
</div>
})
return <div>{output}</div>
}
const Info = React.memo(InfoImplementation)
function midiSend(onOff, { output, note, velocity, activateAfter }) {
if (note > 12 || note < 0) { throw new Error('note') }
const pitch = note + 60
const timestamp = activateAfter ? window.performance.now() + activateAfter : undefined
output.send([onOff, pitch, velocity], timestamp)
}
// https://webmidi-examples.glitch.me/
const midiNoteOn = (config) => midiSend(0x90, config)
const midiNoteOff = (config) => midiSend(0x80, config)
function App() {
const [playingNotes, setPlayingNotes] = useState([])
const { outputs } = useMIDI()
if (outputs.length < 1) return <div>No MIDI Outputs</div>;
const output = R.last(outputs)
const velocity = 100
const midiNotesOn = notes => {
notes.forEach((note, index) => {
midiNoteOn({ output, note, velocity, activateAfter: index * 500 })
})
}
const midiNotesOff = notes => {
notes.forEach((note, index) => {
midiNoteOff({ output, note, velocity, activateAfter: undefined })
})
}
const handlePlay = (chord, addToExisting) => {
const notes = binaryToIndexesOfEnabledNotes(chord)
if (addToExisting) {
midiNotesOn(notes)
setPlayingNotes(R.uniq(playingNotes.concat(notes)))
return
}
midiNotesOff(playingNotes)
midiNotesOn(notes)
setPlayingNotes(notes)
}
const handleRemoveAll = () => {
midiNotesOff([0,1,2,3,4,5,6,7,8,9,10,11,12])
setPlayingNotes([])
}
return <div>
<div style={
{"backgroundColor":"#314963","height":"40px","width":"40px","borderRadius":"100%","position":"fixed","bottom":"21px","right":"25px"}
} onClick={handleRemoveAll}></div>
<div>Using {output.name}</div>
<Info onPlay={handlePlay}/>
</div>
}
ReactDOM.render(
<StyledEngineProvider injectFirst>
<App/>
</StyledEngineProvider>,
document.querySelector("#root")
)
// const groupByLength = scalesWithInfo => {
// scalesWithInfo = R.groupBy(x => x.pitchClassSet.length, scalesWithInfo)
// console.log(scalesWithInfo)
// scalesWithInfo = R.toPairs(scalesWithInfo).map(([l, infos]) => [l, infos.length])
// scalesWithInfo = R.fromPairs(scalesWithInfo)
// return scalesWithInfo
// }
// assert(groupByLength(scalesWithInfo), {
// 1: 1,
// 2: 11,
// 3: 55,
// 4: 165,
// 5: 330,
// 6: 462,
// 7: 462,
// 8: 330,
// 9: 165,
// 10: 55,
// 11: 11,
// 12: 1,
// })
// assert(groupByLength(scalesWithInfo.filter(x => R.all(distance => distance <=4, x.pitchClassSet))), {
// 3: 1,
// 4: 31,
// 5: 155,
// 6: 336,
// 7: 413,
// 8: 322,
// 9: 165,
// 10: 55,
// 11: 11,
// 12: 1,
// })
@iring-axonify
Copy link

Fantastic work here!
The only concern I have is how some things are named, which may cause or be a result of misunderstanding music theory jargon.

What you've named "IndexesOfEnabledNotes" -- that is a Pitch Class Set. A pitch class set is an array, like [0,2,4,5,7,9,11] (that's the major scale), and it is literally an index of enabled notes.

The thing you've called a pitch class set, [2,2,1,2,2,2,1] is not how one would normally serialize a pitch class set... what you have there is known as the "Interval Structure". It describes the interval between one tone and the next.

Lastly, the constant you've named "numberOfNotesForPerfectUnison", that's typically known as an "Interval of Equivalence", in case you want something only slightly more succinct and common in the music theory literature.

I should also warn that many scale name collections you'll find on the www are full of errors. I didn't proof read your list there but in my experience inaccurate scale names are endemic because the same lists keep getting copied and pasted again and again, errors intact. I have spent approx 5 years correcting them in my own collection, and I'm still not 100% certain they are perfectly error-free.

Also, for the sake of not perpetuating racial slurs just replace "gypsy" with "romani" wherever it appears. Cheers!!

@srghma
Copy link
Author

srghma commented Apr 2, 2022

@iring-axonify oh god, I love your site!
It's is like to be noticed by Einstein, O_O

Will fix)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment