index.js (8552B)
1 import { h, Component } from 'preact'; 2 import WaveEditor from '../WaveEditor/'; 3 import WaveManager from '../WaveManager/'; 4 import Synth from '../Synth/'; 5 import CircleButton from '../CircleButton/'; 6 import HSlider from '../HSlider/'; 7 import Param from '../Param/'; 8 import Wheel from '../Wheel/'; 9 import Help from '../Help/'; 10 import Keybindings from '../Keybindings/'; 11 import WaveTable from '../WaveTable/'; 12 import './App.css'; 13 import '../iconfont/icons.css'; 14 import consts from '../consts.js'; 15 import helpers from '../helpers.js'; 16 17 const Adsr = (props) => ( 18 <div class="adsr"> 19 {consts.adsrProperties.map((aspect) => ( 20 <Param 21 suffix={aspect.suffix || ''} 22 precision={1} 23 val={props.adsr[aspect.name]} 24 name={aspect.name} 25 minVal={0} 26 maxVal={aspect.maxVal} 27 update={(newVal) => {props.update(aspect.name, newVal)}} 28 > 29 <Wheel percent={props.adsr[aspect.name] / aspect.maxVal} /> 30 </Param> 31 ))} 32 </div> 33 ) 34 35 class App extends Component { 36 editingWaveform = () => this.props.tones[this.props.editingToneIdx].waveform 37 38 //TODO these can be in redux too, using reselect 39 activeTones = () => { 40 let hasSolo = false; 41 const waves = this.props.tones.reduce((accum, val) => { 42 let group = 'rest'; 43 if (val.solo) { 44 hasSolo = true; 45 group = 'solo'; 46 } 47 if (!this.props.playing || val.beats[this.props.beat]) { 48 if (!val.mute) { 49 accum[group].push(val); 50 } 51 } 52 return accum; 53 }, {solo: [], rest: []}); 54 return hasSolo ? waves.solo : waves.rest; 55 } 56 57 totalWaveform = () => { 58 const tones = this.activeTones(); 59 if (tones.length === 0) { 60 return new Array(consts.BUF_SIZE).fill(0); 61 } 62 const runningAverage = (curVal, valToAdd, iteration) => 63 (((curVal * iteration) + valToAdd) / (iteration + 1)); 64 const firstTone = tones.shift(); 65 const w = tones.reduce( 66 (totalWaveform, currTone, i) => ( 67 totalWaveform.map( 68 (val, j) => ( 69 runningAverage( 70 val, 71 currTone.waveform[j] * currTone.mix, 72 i + 1 73 ) 74 ) 75 ) 76 77 ), 78 helpers.scale(firstTone.waveform, firstTone.mix) 79 ); 80 return helpers.scale(w, 1 / w.reduce((l, r) => Math.max(Math.abs(l), Math.abs(r)))); 81 } 82 83 keyHandler(e) { 84 //TODO handle global commands, maybe some modal stuff even wow 85 console.log('wow i got through', e.key); 86 } 87 88 render() { 89 const tones = this.props.tones.map((form, idx) => { 90 return ( 91 <WaveManager 92 activate={this.props.setEditingToneIdx.bind(null, idx)} 93 remove={this.props.deleteTone.bind(null, idx)} 94 duplicate={() => { 95 let pleaseActivate = false; 96 if (this.props.editingToneIdx === idx) { 97 pleaseActivate = true; 98 } 99 this.props.addTone(this.props.tones[idx].waveform.slice(), idx + 1, pleaseActivate); 100 }} 101 activated={idx === this.props.editingToneIdx} 102 tone={this.props.tones[idx]} 103 beat={this.props.beat} 104 numBeats={this.props.numBeats} 105 toggleMute={() => { 106 this.props.setToneProperty(idx, 'mute', !this.props.tones[idx].mute); 107 }} 108 toggleSolo={() => { 109 this.props.setToneProperty(idx, 'solo', !this.props.tones[idx].solo); 110 }} 111 updateBeat={(i, val) => { 112 this.props.setToneProperty( 113 idx, 114 'beats', 115 helpers.boolArray.update( 116 this.props.tones[idx].beats, 117 i, 118 val 119 ) 120 ); 121 }} 122 mix={this.props.tones[idx].mix} 123 updateMix={(mix) => {this.props.setToneProperty(idx, 'mix', mix)}} 124 ></WaveManager> 125 ); 126 }) 127 const Synthing = ( 128 <div 129 class="App" 130 onKeyDown={this.keyHandler} 131 > 132 <div> 133 <h1 id="synthing-title"> synthing </h1> 134 <div class="wave-scroller"> 135 <div class={`total-wave${this.props.playing ? ' -move' : ''}`}> 136 <WaveTable width={100} height={50} waveform={this.totalWaveform()} /> 137 <WaveTable width={100} height={50} waveform={this.totalWaveform()} /> 138 <WaveTable width={100} height={50} waveform={this.totalWaveform()} /> 139 </div> 140 </div> 141 </div> 142 <WaveEditor 143 mouseData={this.state.mouseData} 144 waveform={this.editingWaveform()} 145 updateWaveform={(waveform) => { 146 this.props.setToneProperty(this.props.editingToneIdx, 'waveform', waveform); 147 }} 148 ></WaveEditor> 149 <div class="global-controls"> 150 <div class="play-container"> 151 <CircleButton 152 active={this.props.playing} 153 action={this.props.startMetro} 154 disabled={this.props.playing} 155 > 156 <div class="triangle"></div> 157 </CircleButton> 158 <CircleButton 159 active={!this.props.playing} 160 action={this.props.stopMetro} 161 > 162 <div class="rectangle"></div> 163 </CircleButton> 164 <Param 165 precision={0} 166 name="bpm" 167 minVal={20} 168 maxVal={600} 169 val={this.props.bpm} 170 update={this.props.setBpm} 171 /> 172 <Param 173 name="beats" 174 minVal={3} 175 maxVal={16} 176 val={this.props.numBeats} 177 update={this.props.setNumBeats} 178 /> 179 </div> 180 <div class="adsr-container"> 181 <Adsr adsr={this.props.adsr} update={this.props.setAdsrProperty} /> 182 <HSlider value={this.props.volume} update={this.props.setVolume} /> 183 </div> 184 </div> 185 <div class="wave-manager-container"> 186 {tones} 187 </div> 188 <button class="add-button" onClick={() => this.props.addTone()}> 189 <span>+</span> 190 </button> 191 <Synth 192 waveform={this.totalWaveform()} 193 volume={this.props.volume} 194 adsr={this.props.adsr} 195 ></Synth> 196 <Help /> 197 <Keybindings /> 198 </div> 199 ); 200 const MobileBlockade = ( 201 <div> 202 <h1>Visit Synthing on your laptop or desktop</h1> 203 <h2>Or borrow one from a friend and use it together</h2> 204 <h3>Synthing requires a mouse and a hardware keyboard for now</h3> 205 <h4>Thanks {":)"}</h4> 206 </div> 207 ); 208 // I'm sorry, but I must sniff. It's not about screen size, it's about the hardware. 209 return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i 210 .test(navigator.userAgent) ? MobileBlockade : Synthing; 211 } 212 }; 213 214 export default App;