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,
// })
@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