synthing

a waveform sequencing synth on the web
Log | Files | Refs | Submodules

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;