Last active
February 14, 2019 06:54
-
-
Save gaearon/d62e0b84a0df5bc393cd1d56777cebdc to your computer and use it in GitHub Desktop.
Marble-style sequencer + sampler for https://github.com/FormidableLabs/react-music
This file contains 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
/* | |
This replaces <Sequencer> + multiple <Sampler>s with a marble diagram sequencer. | |
You can use it like this: | |
<Marble | |
resolution={16} | |
samples={[ | |
'samples/kick.wav', | |
'samples/snare.wav', | |
]} | |
diagrams={[ | |
'x-x- ---- x-x- ----', | |
'---- x--- ---- x---', | |
]} | |
/> | |
Whitespace is ignored in the diagrams. | |
By default, "x" means play and "-" means skip. | |
However you can also define custom "expansions". | |
Expansions are the letters that "expand" to the patterns you define. | |
They will be interpolated into the track according to the free space you left out from its resolution. | |
For example: | |
<Marble | |
resolution={16} | |
samples={[ | |
'samples/kick.wav', | |
'samples/snare.wav', | |
'samples/snare.wav', | |
]} | |
diagrams={[ | |
'x-x- ---- x-x- ----', | |
'---- x--- ---- x---', | |
'---- ---- ---- o ', | |
]} | |
expansions={{ | |
o: 'xxx', | |
}} | |
/> | |
Since we know the resolution is 16 but the third diagram only contains 12 "resolved" characters, | |
the 2 other beats are distributed between expansion characters. We only have one of them ("o") | |
at the very end, so it gets replaced with "xxx" which is spread over 2 free beats. Dubstep! | |
Expansions can use other expansions, and time will be allocated recursively by the same algorithm. | |
For example: | |
<Marble | |
resolution={16} | |
samples={[ | |
'samples/kick.wav', | |
'samples/snare.wav', | |
'samples/snare.wav', | |
]} | |
diagrams={[ | |
'x-x- ---- x-x- ----', | |
'---- x--- ---- x---', | |
'---- ---- ---- o ', | |
]} | |
expansions={{ | |
o: 'p-t', | |
p: 'x--', | |
t: 'xxx', | |
}} | |
/> | |
Be careful with expansions: if you have a loop between them, the stack will overflow. | |
Have fun! | |
*/ | |
import React from 'react'; | |
import { Sampler, Sequencer } from 'react-music'; | |
const parseBeats = (diagram, expansions) => { | |
const beats = diagram.replace(/\s/g, '').split(''); | |
for (let i = 0; i < beats.length; i++) { | |
const char = beats[i]; | |
if (char === '-') { | |
beats[i] = false; | |
} else if (char === 'x') { | |
beats[i] = true; | |
} else { | |
beats[i] = parseBeats(expansions[char], expansions); | |
} | |
} | |
return beats; | |
}; | |
const getExpansionResolution = (beats, resolution) => { | |
const resolvedCount = beats.filter((c) => typeof c === 'boolean').length; | |
const spaceForExpansions = resolution - resolvedCount; | |
const expansionCount = beats.length - resolvedCount; | |
const spacePerExpansion = spaceForExpansions / expansionCount; | |
return spacePerExpansion; | |
}; | |
const convertBeatsToSteps = (beats, resolution) => { | |
const steps = []; | |
const expansionResolution = getExpansionResolution(beats, resolution); | |
for (let i = 0; i < beats.length; i++) { | |
const beat = beats[i]; | |
if (beat === true) { | |
steps.push(i); | |
} else if (beat === false) { | |
continue; | |
} else { | |
const expansionSteps = convertBeatsToSteps(beat, expansionResolution); | |
const interpolatedSteps = expansionSteps.map((relativeStep) => | |
i + relativeStep / expansionSteps.length * expansionResolution | |
); | |
steps.push(...interpolatedSteps); | |
} | |
} | |
return steps; | |
}; | |
const parseDiagram = (diagram, resolution, expansions) => { | |
const beats = parseBeats(diagram, expansions); | |
return convertBeatsToSteps(beats, resolution); | |
}; | |
const Marble = ({ | |
children, | |
diagrams, | |
expansions, | |
resolution, | |
samples, | |
}) => ( | |
<Sequencer | |
resolution={resolution} | |
bars={1} | |
> | |
{diagrams.map((diagram, index) => | |
<Sampler | |
key={index} | |
sample={samples[index]} | |
steps={parseDiagram(diagram, resolution, expansions)} | |
> | |
{children} | |
</Sampler> | |
)} | |
</Sequencer> | |
); | |
Marble.defaultProps = { | |
expansions: {}, | |
}; | |
Marble.propTypes = { | |
children: React.PropTypes.node, | |
diagrams: React.PropTypes.arrayOf( | |
React.PropTypes.string.isRequired | |
).isRequired, | |
expansions: React.PropTypes.objectOf( | |
React.PropTypes.string.isRequired | |
).isRequired, | |
resolution: React.PropTypes.number.isRequired, | |
samples: React.PropTypes.arrayOf( | |
React.PropTypes.string.isRequired | |
).isRequired, | |
}; |
It is mandatory in the sense that the component code doesn’t work without it.
I prefer to mark it as required in this case.
If you omit it React won’t complain because defaultProps
will still supply it.
Hey,
I always use Default props only for optional props. The way I see a Component and its required props are like a function with arguments. Take 'sqrt(x)' func for example, i have to pass 'x' to it to make it work or else it should fail
Here in this case, If I don't provide the expansions, my component will not break as it takes its value from default props. Doesn't it kill the whole point of calling expansions a 'required' or 'mandatory'?
Would love to hear your opinion on this?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
if expansions is a mandatory props then why it gets assigned as a default props?