A really simple React.js wrapper around a Tone.js Oscillator.
Monophonic (only plays one note at a time)
Using my synthesizer React.js components for the UI
A Pen by Gregor Adams on CodePen.
A really simple React.js wrapper around a Tone.js Oscillator.
Monophonic (only plays one note at a time)
Using my synthesizer React.js components for the UI
A Pen by Gregor Adams on CodePen.
//console.clear(); | |
class Oscillator extends React.Component { | |
constructor(props) { | |
super(props); | |
this.env = this.props.envelope; | |
this.waves = ['sine','square','triangle','sawtooth']; | |
this.tone = new Tone.Oscillator({ | |
frequency: this.props.frequency, | |
type: this.waves[this.props.waveform], | |
volume: this.props.volume | |
}).connect(this.env).start(); | |
} | |
componentWillReceiveProps(newProps) { | |
this.tone.detune.value = newProps.detune; | |
this.tone.volume.value = newProps.volume; | |
this.tone.type = this.waves[newProps.waveform]; | |
if (newProps.playing ) { | |
this.tone.frequency.value = newProps.playing; | |
} | |
} | |
render() { | |
let ret; | |
return (<div className="oscillator"> | |
<br/> | |
{ this.props.children } | |
</div>); | |
} | |
} | |
class Synth extends React.Component { | |
constructor(props) { | |
super(props); | |
this.envelope = new Tone.AmplitudeEnvelope({ | |
attack : 0.41, | |
decay : 0.21, | |
sustain : 0.9, | |
release : .9 | |
}).toMaster() | |
this.state = { | |
frequencies: { | |
0: 440 | |
}, | |
detunes: { | |
0: 0 | |
}, | |
volumes: { | |
0: -20 | |
}, | |
waveforms: { | |
0: 1 | |
} | |
}; | |
this.setDetune = this.setDetune.bind(this); | |
this.setVol = this.setVol.bind(this); | |
this.setWav = this.setWav.bind(this); | |
this.startNote = this.startNote.bind(this); | |
this.stopNote = this.stopNote.bind(this); | |
} | |
setDetune(osc, v) { | |
let detunes = this.state.detunes; | |
detunes[osc] = v; | |
this.setState({ | |
detunes: detunes | |
}); | |
} | |
setVol(osc, v) { | |
let volumes = this.state.volumes; | |
volumes[osc] = v; | |
this.setState({ | |
volumes: volumes | |
}); | |
} | |
setWav(osc, v) { | |
let waveforms = this.state.waveforms; | |
waveforms[osc] = v; | |
console.log(v); | |
this.setState({ | |
waveforms: waveforms | |
}); | |
} | |
startNote(note) { | |
this.setState({playing: note}); | |
this.envelope.triggerAttack(); | |
} | |
stopNote(note) { | |
this.setState({playing: false}); | |
this.envelope.triggerRelease(); | |
} | |
render() { | |
return ( | |
<div className='synth'> | |
<Oscillator frequency={440} | |
detune={ this.state.detunes[0] } | |
waveform={ this.state.waveforms[0] } | |
volume={ this.state.volumes[0] } | |
type={ 'square' } | |
envelope={this.envelope} | |
playing={this.state.playing}> | |
<Poti className='_colored orange' | |
range={[-50,50]} | |
size={60} | |
label={'detune'} | |
markers={21} | |
fullAngle={300} | |
steps={[{label:-10},{label:-5},{label:'0'},{label:5},{label:10}]} | |
onUpdate={ this.setDetune.bind(this, 0) } | |
value={ this.state.detunes[0]} /> | |
<Poti className='_colored yellow' | |
range={[0,3]} | |
size={60} | |
label={'waveform'} | |
snap={true} | |
fullAngle={300} | |
steps={[{label:'sin'},{label:'sqr'},{label:'tri'},{label:'saw'}]} | |
onUpdate={ this.setWav.bind(this, 0) } | |
value={ this.state.waveforms[0]} /> | |
<Poti className='_colored red' | |
range={[-50,20]} | |
size={60} | |
label={'volume'} | |
markers={21} | |
fullAngle={300} | |
steps={[{label:'min'},{},{},{},{},{},{},{},{},{},{label:'max'}]} | |
onUpdate={ this.setVol.bind(this, 0) } | |
value={ this.state.volumes[0]} /> | |
</Oscillator> | |
<Keyboard onDown={this.startNote} onUp={this.stopNote}/> | |
</div>); | |
} | |
} | |
ReactDOM.render(<Synth/>, document.body); |
<script src="//cdn.rawgit.com/JedWatson/classnames/master/index.js"></script> | |
<script src="//cdn.rawgit.com/Tonejs/Tone.js/9bf00eb49be5b97b7446c833657bcd492c04b208/build/Tone.min.js"></script> | |
<script src="//codepen.io/pixelass/pen/YwvYPL.js"></script> | |
<script src="//codepen.io/pixelass/pen/VedyyE.js"></script> | |
<script src="//cdn.rawgit.com/rofrischmann/inline-style-prefixer/43994c348ee869fcb656c4b79873e5605fba2e3e/dist/inline-style-prefixer.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script> |
$stroke: mix(#fefedc, #000, 70%); | |
$key-height: 105px; | |
$key-width: 40px; | |
$roundness: 2px; | |
$d3-inensity: 20px; | |
$key-chip: false; // false or px value | |
.Keyboard { | |
position: relative; | |
overflow: hidden; | |
background: black; | |
user-select: none; | |
height: $key-height * 2 + $d3-inensity / 2 + 2; | |
&:before, | |
&:after { | |
content: ''; | |
pointer-events: none; | |
position: absolute; | |
left: 0; | |
width: 100%; | |
z-index: 2; | |
} | |
&:before { | |
box-shadow: 0 4px 2px #d52, 0 4px 0 #a55; | |
height: 4px; | |
top: -4px; | |
} | |
&:after { | |
top: 0; | |
height: 100%; | |
border-radius: 5px; | |
box-shadow: 0 0 0 2px black inset, 0 0 0 4px black; | |
} | |
.key { | |
cursor: pointer; | |
transform: translate3d(0, 0, 0); | |
border-style: none !important; | |
.top { | |
border-style: none !important; | |
} | |
} | |
.black { | |
z-index: 0; | |
background-color: #111; | |
background-image: linear-gradient(to bottom, rgba(white, 0.1), rgba(black, 0.2)), radial-gradient(ellipse at center, rgba(black, 0.8), rgba(black, 0.1)), linear-gradient(to top, rgba(black, 0.2), rgba(black, 0.1)), linear-gradient(to right, rgba(black, 0.3) 0px, rgba(white, 0.3) 1px, rgba(black, 0) 6px), linear-gradient(to left, rgba(black, 0.3) 0px, rgba(white, 0.3) 1px, rgba(black, 0) 6px); | |
box-shadow: 0 0 0 1px rgba(black, 0.1) inset, 1px 0 0 0 rgba(#333, 1) inset, 0 1px 2px rgba($stroke, 0.4), 0 0 0 2px #000, 0 ($d3-inensity * 2) 0 2px #333; | |
margin-top: $d3-inensity * -0.5!important; | |
&._active { | |
transform: translate3d(0, $d3-inensity * -1.125, 0); | |
} | |
&:before { | |
content: ''; | |
position: absolute; | |
top: 100%; | |
right: 100%; | |
border-radius: $roundness; | |
margin-right: -1px; | |
margin-top: -2px; | |
border: 10px solid transparent; | |
border-bottom-color: #000; | |
border-right-color: #000; | |
transform-origin: 100% 0%; | |
transform: rotate(15deg); | |
} | |
&:after { | |
content: ''; | |
position: absolute; | |
top: 100%; | |
left: 100%; | |
border-radius: $roundness; | |
margin-left: -1px; | |
margin-top: -2px; | |
border: 10px solid transparent; | |
border-bottom-color: #000; | |
border-left-color: #000; | |
transform-origin: 0% 0%; | |
transform: rotate(-15deg); | |
} | |
} | |
$chip: (); | |
@if $key-chip { | |
$chip: 0 $key-chip 0 0 mix(#F1F1DA, #000, 80%), 0 ($key-chip + 2) 5px -1px mix(#F1F1DA, #000, 40%); | |
} | |
.white { | |
z-index: 1; | |
background-color: #EDED68; | |
background-image: linear-gradient(to bottom, rgba(white, 1), rgba(white, 0.8)), radial-gradient(ellipse at center, rgba(black, 0.5), rgba(black, 0.1)), linear-gradient(to top, rgba(black, 0.2), rgba(black, 0.1)), linear-gradient(to right, rgba(black, 0.1), rgba(white, 0.2), rgba(black, 0.1)); | |
background-position: center center, 15px 50px, center center; | |
background-size: 100% 100%, 300% 100%, 100% 100%; | |
box-shadow: 0 0 0 1px rgba(black, 0.1) inset, 1px 0 0 0 rgba(#FFF, 1) inset, 0 0 0 1px rgba($stroke, 1), $chip, 0 $d3-inensity 0 -1px mix(#F1F1DA, #000, 70%), 0 $d3-inensity 0 0 mix(#F1F1DA, #000, 80%), 0 $d3-inensity 0 1px rgba($stroke, 1); | |
&._active { | |
background-size: 100% 100%,100% 100%,100% 100%, 300% 100%, 100% 100%; | |
background-position: center center, center center, center center, 15px 50px, center center; | |
background-image: linear-gradient(to bottom, rgba(white, 1), rgba(white, 0.7)), linear-gradient(to right, rgba(black, 0.5), rgba(black, 0) 8px), linear-gradient(to left, rgba(black, 0.5), rgba(black, 0) 6px), radial-gradient(ellipse at center, rgba(black, 0.5), rgba(black, 0.1)), linear-gradient(to top, rgba(black, 0.2), rgba(black, 0.1)), linear-gradient(to right, rgba(black, 0.1), rgba(white, 0.2), rgba(black, 0.1)); | |
} | |
.top { | |
box-shadow: 1px 0 0 0 rgba(black, 0.1) inset, 1px 0 0 0 rgba(#FFF, 1) inset, 0 -1px 0 0 rgba(#FFF, 1), 0 -1px 0 1px rgba($stroke, 1); | |
margin-bottom: -1px; | |
&:before, &:after { | |
position: absolute; | |
bottom: 0; | |
height: $roundness * 2; | |
width: $roundness * 2; | |
margin-bottom: 2px; | |
} | |
&:before { | |
left: 100%; | |
border-radius: 0 0 0 $roundness; | |
margin-left: 1px; | |
box-shadow: -1px 0 0 0 $stroke, 0 1px 0 0 $stroke, -3px 3px 0 #fff; | |
} | |
&:after { | |
right: 100%; | |
margin-right: 1px; | |
border-radius: 0 0 $roundness 0; | |
box-shadow: 1px 0 0 0 $stroke, 0 1px 0 0 $stroke, 3px 3px 0 #fff; | |
} | |
} | |
&.left { | |
.top { | |
&:before { | |
content: ''; | |
} | |
} | |
} | |
&.right { | |
.top { | |
&:after { | |
content: ''; | |
} | |
} | |
} | |
&.center { | |
border-radius: $roundness; | |
.top { | |
&:before, &:after { | |
content: ''; | |
} | |
} | |
} | |
} | |
} | |
$colors: (red: red, orange: orange, yellow: yellow, white: white, grey: #ccc, dark-grey: #1E1E1E, black: #111, background: #28282B); | |
@function color($name) { | |
@return map-get($colors, $name); | |
} | |
@mixin Poti($color: false) { | |
$className: ".Poti"; | |
@if $color { | |
$className: $className + "._colored"; | |
$className: $className + ".#{$color}"; | |
} | |
$className: join($className, ","); | |
#{$className} { | |
@content; | |
} | |
} | |
@include Poti { | |
color: color(white); | |
.PotiIndicator { | |
color: transparent; | |
&:before { | |
content: ''; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
height: 2px; | |
width: 2px; | |
background: color(white); | |
margin-bottom: 5px; | |
border-radius: 100%; | |
} | |
.label { | |
color: color(white); | |
} | |
&._numbered { | |
&:before { | |
content: normal; | |
} | |
} | |
} | |
.PotiMarker { | |
z-index: 2; | |
color: color(red); | |
font-size: 6px; | |
will-change: transform; | |
} | |
.PotiKnob { | |
z-index: 2; | |
box-shadow: 0 0 2px 1px rgba(color(white), 0.1) inset, 0 0 0 2px color(black) inset; | |
margin-top: -5px; | |
&:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 50%; | |
transform: translateX(-50%); | |
height: 100%; | |
width: 110%; | |
padding: 4px 0; | |
border-radius: 100%; | |
box-shadow: 0 2px 0 1px color(black), 0 2px 0 6px color(dark-grey), 0 2px 0 7px color(black); | |
background-color: color(dark-grey); | |
background-image: linear-gradient(to top, rgba(black, 0.5) 0, transparent 10px), linear-gradient(to right, transparent 20%, rgba(white, 0.2) 40%, transparent 70%); | |
} | |
&:after { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
height: 100%; | |
width: 100%; | |
border-radius: 100%; | |
background-color: color(dark-grey); | |
background-image: radial-gradient(closest-corner, rgba(color(black), 0.2) 10%, rgba(color(black), 0.1) 60%, rgba(color(white), 0.1) 80%); | |
box-shadow: inherit; | |
} | |
} | |
.PotiMarker { | |
z-index: 2; | |
color: color(red); | |
font-size: 6px; | |
will-change: transform; | |
} | |
&._colored { | |
.PotiIndicator { | |
&:before { | |
height: 4px; | |
margin-bottom: 2px; | |
border-radius: 0; | |
} | |
} | |
.PotiKnob { | |
z-index: 2; | |
box-shadow: 0 0 0 2px color(dark-grey) inset; | |
margin-top: -5px; | |
background: currentColor; | |
border-width: 0; | |
position: relative; | |
&:before { | |
height: 120%; | |
width: 110%; | |
padding: 1px; | |
box-shadow: 0 1px 0 1px color(black); | |
top: 0; | |
} | |
&:after { | |
background-color: inherit; | |
} | |
} | |
.PotiMarker { | |
z-index: 2; | |
color: color(black); | |
font-size: 6px; | |
} | |
} | |
} | |
@include Poti(orange) { | |
.PotiKnob { | |
color: color(orange); | |
} | |
} | |
@include Poti(red) { | |
.PotiKnob { | |
color: color(red); | |
} | |
} | |
@include Poti(yellow) { | |
.PotiKnob { | |
color: color(yellow); | |
} | |
} | |
.oscillator { | |
border-radius: 4px; | |
display: block; | |
background: #333; | |
padding: 10px; | |
margin: 40px 0; | |
box-shadow: 0 0 0 1px rgba(black,0.3) inset; | |
} | |
.synth { | |
position: relative; | |
margin: 50px; | |
border-radius: 4px; | |
display: inline-block; | |
padding: 0 20px; | |
box-shadow: 0 0 0 2px rgba(black,0.3) inset; | |
background-color: hsl(20,60%,20%); | |
background-image: | |
radial-gradient(10px 90px, 6px 120px, rgba(black,0) 10%,rgba(black,0.05) 10%,rgba(black,0.05) 40%,rgba(white,0) 40%,rgba(white,0.05) 60%,rgba(black,0) 60%), | |
radial-gradient(70px 130px, 8px 180px, rgba(black,0) 10%,rgba(black,0.05) 10%,rgba(black,0.05) 40%,rgba(white,0) 40%,rgba(white,0.05) 60%,rgba(black,0) 60%), | |
radial-gradient(20px 120px, 6px 120px, rgba(black,0) 10%,rgba(black,0.05) 10%,rgba(black,0.05) 40%,rgba(white,0) 40%,rgba(white,0.05) 60%,rgba(black,0) 60%), | |
linear-gradient(to right, rgba(black,0) 10%,rgba(black,0.05) 15%,rgba(black,0) 40%,rgba(white,0.05) 60%,rgba(black,0) 70%), | |
linear-gradient(to right, rgba(black,0) 10%,rgba(black,0.05) 10%,rgba(black,0) 50%,rgba(white,0.05) 60%,rgba(black,0) 65%), | |
linear-gradient(to right, rgba(black,0) 10%,rgba(black,0.05) 10%,rgba(black,0) 55%,rgba(white,0.05) 60%,rgba(black,0) 60%), | |
linear-gradient(to top right, rgba(black,0.2),rgba(black,0) 60%); | |
background-size: 250px 200px, 120px 300px,100px 500px,8px 100%, 12px 100%, 20px 100%, 100% 100%; | |
box-shadow: 0 -26px 0 0 rgba(black,0.5) inset, | |
0 -26px 10px 0 rgba(white,0.2) inset; | |
&:before, &:after { | |
content: ''; | |
position: absolute; | |
top: -3px; | |
bottom: -3px; | |
width: 40px; | |
background-color: hsl(20,60%,25%); | |
background-image: inherit; | |
background-size: inherit; | |
border-radius: 3px; | |
} | |
&:before { | |
left: -30px; | |
box-shadow: 0 -32px 0 0 rgba(black,0.5) inset, | |
0 -32px 10px 0 rgba(white,0.2) inset, | |
10px 0 10px -10px rgba(black,0.6); | |
} | |
&:after { | |
right: -30px; | |
box-shadow: 0 -32px 0 0 rgba(black,0.5) inset, | |
0 -32px 10px 0 rgba(white,0.2) inset, | |
-10px 0 10px -10px rgba(black,0.6); | |
} | |
} |