Skip to content

Instantly share code, notes, and snippets.

@michaelforrest
Created November 24, 2020 13:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaelforrest/d337afa96b325282085089d964f61584 to your computer and use it in GitHub Desktop.
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
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