Skip to content
This repository has been archived by the owner on Nov 10, 2017. It is now read-only.

Commit

Permalink
Allow BPM update, highlight current beat.
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 committed Oct 25, 2016
1 parent 4c682fa commit 0cab621
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 55 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"name": "tinysynth",
"version": "0.1.0",
"private": true,
"homepage": "http://n1k0.github.io/tinysynth",
"scripts": {
"deploy": "gh-pages -d build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
Expand All @@ -15,6 +17,7 @@
},
"devDependencies": {
"flow-bin": "^0.33.0",
"gh-pages": "^0.11.0",
"react-scripts": "0.7.0"
}
}
37 changes: 37 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
th {
font-size: .9em;
}

input[type=range] {
width: 80px;
}

.beat {
background: #eee;
text-align: center;
Expand All @@ -9,6 +17,11 @@
background: pink;
}

.beat.current {
background: #f5f5eb;
}

.beat:nth-of-type(3),
.beat:nth-of-type(7),
.beat:nth-of-type(11),
.beat:nth-of-type(15) {
Expand All @@ -20,3 +33,27 @@
width: 40px;
height: 40px;
}

.btn {
display: inline-block;
background: #fff;
border: 1px solid #eee;
border-radius: 2px;
padding: .5em;
color: #777;
}

.bpm input[type=range] {
margin-top: 1em;
}

.bpm input[type=number] {
border: 0;
width: 60px;
font-size: .9em;
margin-top: -1em;
}

.controls .btn {
margin-right: .5em;
}
127 changes: 86 additions & 41 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import type { Track, ToneLoop } from "./types";

import React, { Component } from 'react';
import './App.css';
import React, { Component } from "react";
import "./App.css";

import * as sequencer from "./sequencer";


function initTracks(): Track[] {
return [
{name: "hi-hat (open)", sample: "audio/hihato.wav", vol: .8, muted: false, beats: initBeats(16)},
{name: "hi-hat (close)", sample: "audio/hihatc.wav", vol: .8, muted: false, beats: initBeats(16)},
{name: "snare", sample: "audio/snare.wav", vol: 1, muted: false, beats: initBeats(16)},
{name: "kick", sample: "audio/kick.wav", vol: 1, muted: false, beats: initBeats(16)},
{name: "hi-hat (open)", sample: "audio/hihato.wav", vol: .4, muted: false, beats: initBeats(16)},
{name: "hi-hat (close)", sample: "audio/hihatc.wav", vol: .4, muted: false, beats: initBeats(16)},
{name: "snare", sample: "audio/snare.wav", vol: .9, muted: false, beats: initBeats(16)},
{name: "kick", sample: "audio/kick.wav", vol: .8, muted: false, beats: initBeats(16)},
];
}

Expand Down Expand Up @@ -54,11 +54,12 @@ function _muteTrack(tracks, name) {
});
}

function TrackView({track, toggleTrackBeat, setTrackVolume, muteTrack}: {
track: Track,
toggleTrackBeat: (name: string, beat: number) => void,
setTrackVolume: (name: string, vol: number) => void,
muteTrack: (name: string) => void,
function TrackView({
track,
currentBeat,
toggleTrackBeat,
setTrackVolume,
muteTrack,
}) {
return (
<tr className="track">
Expand All @@ -70,50 +71,81 @@ function TrackView({track, toggleTrackBeat, setTrackVolume, muteTrack}: {
<input type="checkbox" checked={!track.muted}
onChange={event => muteTrack(track.name)} /></td>
{
track.beats.map((v, beat) => (
<td key={beat} className={`beat ${v ? "active" : ""}`}>
<a href="" onClick={(event) => {
event.preventDefault();
toggleTrackBeat(track.name, beat);
}} />
</td>
))
track.beats.map((v, beat) => {
const beatClass = v ? "active" : beat === currentBeat ? "current" : "";
return (
<td key={beat} className={`beat ${beatClass}`}>
<a href="" onClick={(event) => {
event.preventDefault();
toggleTrackBeat(track.name, beat);
}} />
</td>
);
})
}
</tr>
);
}

function TrackListView({tracks, toggleTrackBeat, setTrackVolume, muteTrack}) {
function TrackListView({
tracks,
currentBeat,
toggleTrackBeat,
setTrackVolume,
muteTrack,
}) {
return (
<table>
<tbody>{
tracks.map((track, i) => {
return (
<TrackView key={i}
track={track}
currentBeat={currentBeat}
toggleTrackBeat={toggleTrackBeat}
setTrackVolume={setTrackVolume}
muteTrack={muteTrack} />
);
})
}</tbody>
</table>
);
}

function Controls({bpm, updateBPM, start, stop}) {
const onChange = event => updateBPM(parseInt(event.target.value, 10));
return (
<div>
<h3>tinysynth</h3>
<table>
<tbody>{
tracks.map((track, i) => {
return (
<TrackView key={i}
track={track}
toggleTrackBeat={toggleTrackBeat}
setTrackVolume={setTrackVolume}
muteTrack={muteTrack} />
);
})
}</tbody>
</table>
<div className="controls">
<button className="btn btn-start" onClick={start}>Play</button>
<button className="btn btn-stop" onClick={stop}>Stop</button>
<div className="bpm">
<label>
BPM
<input type="range" min="30" max="240" value={bpm} onChange={onChange}/>
<input type="number" value={bpm} onChange={onChange} />
</label>
</div>
</div>
);
}

class App extends Component {
state: {
bpm: number,
currentBeat: number,
loop: ToneLoop,
tracks: Track[],
};

constructor(props: {}) {
super(props);
const tracks = initTracks();
this.state = {tracks, loop: sequencer.create(tracks)};
this.state = {
bpm: 120,
currentBeat: -1,
tracks, loop:
sequencer.create(tracks, this.updateCurrentBeat),
};
}

start = () => {
Expand All @@ -122,13 +154,18 @@ class App extends Component {

stop = () => {
this.state.loop.stop();
this.setState({currentBeat: -1});
};

updateCurrentBeat = (beat: number): void => {
this.setState({currentBeat: beat});
};

updateTracks = (newTracks: Track[]) => {
const {loop} = this.state;
this.setState({
tracks: newTracks,
loop: sequencer.update(loop, newTracks),
loop: sequencer.update(loop, newTracks, this.updateCurrentBeat),
});
};

Expand All @@ -144,20 +181,28 @@ class App extends Component {

muteTrack = (name: string) => {
const {tracks} = this.state;
this.updateTracks( _muteTrack(tracks, name));
this.updateTracks(_muteTrack(tracks, name));
};

updateBPM = (newBpm: number) => {
const {bpm} = this.state;
sequencer.updateBPM(bpm);
this.setState({bpm: newBpm});
}

render() {
const {tracks} = this.state;
const {bpm, currentBeat, tracks} = this.state;
const {updateBPM, start, stop} = this;
return (
<div>
<h3>tinysynth</h3>
<TrackListView
tracks={tracks}
currentBeat={currentBeat}
toggleTrackBeat={this.toggleTrackBeat}
setTrackVolume={this.setTrackVolume}
muteTrack={this.muteTrack} />
<button onClick={this.start}>start</button>
<button onClick={this.stop}>stop</button>
<Controls {...{bpm, updateBPM, start, stop}} />
</div>
);
}
Expand Down
21 changes: 13 additions & 8 deletions src/sequencer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow */
import type { Track, ToneLoop } from "./types";
import type { Track, ToneLoop, BeatNotifier } from "./types";

import Tone from "tone";

Expand All @@ -11,9 +11,9 @@ const velocities = [
1, .5, .75, .5,
];

export function create(tracks: Track[]): ToneLoop {
export function create(tracks: Track[], beatNotifier: BeatNotifier): ToneLoop {
const loop = new Tone.Sequence(
loopProcessor(tracks),
loopProcessor(tracks, beatNotifier),
new Array(16).fill(0).map((_, i) => i),
"16n"
);
Expand All @@ -24,19 +24,24 @@ export function create(tracks: Track[]): ToneLoop {
return loop;
}

export function update(loop: ToneLoop, tracks: Track[]): ToneLoop {
loop.callback = loopProcessor(tracks);
export function update(loop: ToneLoop, tracks: Track[], beatNotifier: BeatNotifier): ToneLoop {
loop.callback = loopProcessor(tracks, beatNotifier);
return loop;
}

function loopProcessor(tracks) {
const urls = tracks.reduce((acc, track) => {
return {...acc, [track.name]: track.sample};
export function updateBPM(bpm: number): void {
Tone.Transport.bpm.value = bpm;
}

function loopProcessor(tracks, beatNotifier: BeatNotifier) {
const urls = tracks.reduce((acc, {name, sample}) => {
return {...acc, [name]: sample};
}, {});

const keys = new Tone.MultiPlayer({urls}).toMaster();

return (time, index) => {
beatNotifier(index);
tracks.forEach(({name, vol, muted, beats}) => {
if (beats[index]) {
// XXX "1n" should be set via some "resolution" track prop
Expand Down
15 changes: 9 additions & 6 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/* @flow */

export type BeatNotifier =
(beat: number) => void;

export type ToneLoop = {
start: () => void,
stop: () => void,
callback: (time: number, index: number) => void,
};

export type Track = {
name: string,
sample: string,
vol: number,
muted: boolean,
beats: boolean[],
};

export type ToneLoop = {
start: () => void,
stop: () => void,
callback: (time: number, index: number) => void,
};

0 comments on commit 0cab621

Please sign in to comment.