Created
November 24, 2020 13:35
-
-
Save michaelforrest/d337afa96b325282085089d964f61584 to your computer and use it in GitHub Desktop.
Sequencer for Mutable Instruments Grids. Demo here: https://goodtohear.co.uk/tools/grids-sequencer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const {useState, useEffect} = React | |
const STEPS_PER_SEQUENCE = 32 | |
const INSTRUMENTS = [{ | |
name: "Kick", | |
sound: new Audio("/audio/kick.wav") | |
}, | |
{ | |
name: "Snare", | |
sound: new Audio("/audio/snare.wav") | |
}, | |
{ | |
name: "Hi Hat", | |
sound: new Audio("/audio/hihat.wav") | |
}] | |
// 1 beat is 8 steps, so | |
const bpmToTick = (bpm) => 1000 * 60 / bpm / 8 | |
const stepsRange = new Array(STEPS_PER_SEQUENCE).fill(0) | |
const DEFAULT_SEQUENCE = `// KICKS | |
255, 0, 0, 0, 0, 0, 145, 0, | |
0, 0, 0, 0, 218, 0, 0, 0, | |
72, 0, 36, 0, 182, 0, 0, 0, | |
109, 0, 0, 0, 72, 0, 0, 0, | |
// SNARES | |
36, 0, 109, 0, 0, 0, 8, 0, | |
255, 0, 0, 0, 0, 0, 72, 0, | |
0, 0, 182, 0, 0, 0, 36, 0, | |
218, 0, 0, 0, 145, 0, 0, 0, | |
// HIHATS | |
170, 0, 113, 0, 255, 0, 56, 0, | |
170, 0, 141, 0, 198, 0, 56, 0, | |
170, 0, 113, 0, 226, 0, 28, 0, | |
170, 0, 113, 0, 198, 0, 85, 0 | |
` | |
const styles = { | |
container: { | |
}, | |
row: { | |
display: "flex", | |
}, | |
step: { | |
width: 44, height: 44, | |
backgroundColor: '#cdcdcd', | |
display: "flex", | |
alignItems: "center", | |
justifyContent: "center", | |
margin: 2, | |
borderRadius: 3, | |
color: "white", | |
width: `${100 / STEPS_PER_SEQUENCE}%`, | |
overflow: "hidden", | |
fontSize: 10, | |
}, | |
selected: { | |
backgroundColor: "#ff5555" | |
}, | |
number: { | |
width: 50, | |
marginRight: 10, | |
}, | |
sequence: { | |
width: 260, | |
height: 234, | |
} | |
} | |
const Step = ({instrument,index,step,value,threshold})=> { | |
if(step == index && value > 255 - threshold){ | |
instrument.sound.currentTime = 0 | |
instrument.sound.volume = value > 192 ? 1.0 : 0.6 // accent as per https://github.com/pichenettes/eurorack/blob/master/grids/pattern_generator.cc#L123 | |
instrument.sound.play() | |
} | |
return <div style={{...styles.step, ...(index == step ? styles.selected : {})}}> | |
{value} | |
</div> | |
} | |
const Row = ({step,instrument,sequence,threshold}) => <div style={styles.row}> | |
{stepsRange.map((_, index)=> <Step | |
key={index} | |
index={index} | |
step={step} | |
instrument={instrument} | |
threshold={threshold} | |
value={sequence[index]} | |
/>)} | |
</div> | |
const GridsSequencer = ()=> { // changing bpm will reload this component | |
const [step, setStep] = useState(0) | |
const [playing, setPlaying] = useState(0) | |
const [bpm, setBpm] = useState(120) | |
useEffect( () => { | |
let interval = setInterval(()=>{ | |
if(playing){ | |
setStep((step + 1) % STEPS_PER_SEQUENCE) | |
} | |
}, bpmToTick(bpm || 120)) // interval callback every submeasure | |
return () => clearInterval(interval) | |
}, [step,playing]) | |
const [sequence, setSequence] = useState(DEFAULT_SEQUENCE) | |
const [thresholds, setThresholds] = useState([200,200,200]) | |
useEffect(()=>{ | |
window.onbeforeunload = (e)=>{ | |
if(sequence != DEFAULT_SEQUENCE){ | |
return "Copy and paste the sequence into a text editor to save your work." | |
} | |
} | |
return ()=> window.onbeforeunload = null | |
}, [sequence]) | |
let previousSequence = sequence // wait this isn't right. | |
let parsedSequence | |
let parseError | |
try { | |
let cleaned = sequence.replace(/\/\/.+/g, "").trim() | |
parsedSequence = JSON.parse("[" + cleaned + "]") | |
}catch(error){ | |
parseError = error | |
parsedSequence = previousSequence | |
} | |
return <div style={styles.container}> | |
<button onClick={()=>setPlaying(!playing)}> | |
{playing ? "PAUSE" : "PLAY"} | |
</button> | |
Tempo: | |
<input type="number" value={bpm} onChange={e=>setBpm(e.target.value)} style={styles.number}/> | |
{ | |
thresholds.map((chance,index) => <span key={index}> | |
{INSTRUMENTS[index].name}: | |
<input | |
type="number" | |
value={chance} | |
min={0} | |
max={255} | |
style={styles.number} | |
onChange={event => { | |
setThresholds(thresholds.map((c,i) => | |
Math.min(Math.max(0, i == index ? parseInt(event.target.value) : c), 255) | |
)) | |
}} | |
/> | |
</span>)} | |
{INSTRUMENTS.map((instrument, index) => { | |
const sequenceOffset = index * STEPS_PER_SEQUENCE | |
return <Row | |
key={index} | |
step={step} | |
threshold={thresholds[index]} | |
instrument={instrument} | |
sequence={parsedSequence | |
.slice(sequenceOffset, sequenceOffset + STEPS_PER_SEQUENCE)} | |
/> | |
})} | |
<p> | |
<small>Paste or edit the sequence in this box: </small><br/> | |
<textarea style={{...styles.sequence, outline: parseError == undefined ? "none" : "1px solid red"}} value={sequence} onChange={ | |
(event)=>{ | |
setSequence(event.target.value) | |
} | |
}/> | |
</p> | |
</div> | |
} | |
const container = document.getElementById("app") | |
container && ReactDOM.render(React.createElement(GridsSequencer), container); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment