Skip to content

Commit

Permalink
Added working knob and displayable piano roll
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-van committed Sep 2, 2020
1 parent 43a4885 commit 7c834aa
Show file tree
Hide file tree
Showing 11 changed files with 890 additions and 132 deletions.
11 changes: 8 additions & 3 deletions .eslintrc.js
Expand Up @@ -7,7 +7,8 @@ module.exports = {
'jest/globals': true
},
extends: [
'standard'
'standard',
'plugin:react/recommended'
/*
'plugin:jsdoc/recommended'
*/
Expand All @@ -18,10 +19,14 @@ module.exports = {
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: [
'jest'
'jest',
'react'
/*
'jsdoc'
*/
Expand Down
4 changes: 3 additions & 1 deletion demo/index.html
Expand Up @@ -7,9 +7,11 @@
<title>sai-experiment</title>

<link rel="stylesheet" href="./src/scss/index.scss">
<script src="./src/index.js"></script>
</head>
<body>
<div id="root">
</div>
<script src="./src/index.js"></script>
</body>
</html>

88 changes: 88 additions & 0 deletions demo/src/App.js
@@ -0,0 +1,88 @@

import React from 'react'

import * as sai from '../../src/sai-experiment'

import { Knob, PianoRoll } from './components'

/**
* The app
*/
export default class App extends React.PureComponent {
constructor (props) {
super(props)
this.state = {
knobVal: 0
}
}

componentDidMount () {
console.log('loading track')

const audioCtx = new AudioContext()
const instrument = {
oscillators: [
{
type: 'sine',
gain: 1,
freqOsc: {
type: 'sine',
amount: 0,
frequency: 10
}
}
],
filters: [
{
type: 'highpass',
frequency: 0,
gain: 0,
q: 1000
}
],
attack: 0.05,
decay: 0.05,
sustainLevel: 0.5,
sustainTime: 0.2,
release: 0.1,
gain: 0.2,

noise: 0,
delay: 0,
delayTime: 0.3,
panAmount: 0,
panFrequency: 2
}
const t = new sai.Track(audioCtx, instrument)
t.output.connect(audioCtx.destination)

console.log('trying to get midi access')
window.navigator.requestMIDIAccess().then(function (midiAccess) {
console.log('midi access granted')
const inputs = midiAccess.inputs.values()
for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
input.value.onmidimessage = receiveMessage
}
})

const receiveMessage = function (mm) {
const message = new sai.MidiMessage(mm.data)
console.log(message)
t.midiMessage(message)
}
}

render () {
return <div className="container my-5">
<p>Hello world</p>
<div>
<Knob style={{ height: 50, width: 50 }} value={this.state.knobVal} onValueChange={(v) => {
this.setState({ knobVal: v })
}}></Knob>
</div>
<div>
<PianoRoll style={{ height: 100, width: '100%' }}/>
</div>
</div>
}
}
184 changes: 184 additions & 0 deletions demo/src/components/Knob.js
@@ -0,0 +1,184 @@

import React from 'react'
import PropTypes from 'prop-types'
import * as d3 from 'd3'
import _ from 'lodash'

const rotateMax = 150
const rotateScale = d3.scaleLinear([-1, 1], [-1 * rotateMax, rotateMax])

const init = function (el, {
...other
}) {
const svg = d3.select(el)
.attr('viewBox', [-1, -1, 2, 2])

const g = svg.append('g')
.classed('knob-rotating', true)

g.append('circle')
.classed('knob-big-circle', true)
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 1)

const spotWidth = 0.07

g.append('rect')
.classed('knob-little-spot', true)
.attr('x', -spotWidth / 2)
.attr('y', -0.8)
.attr('width', spotWidth)
.attr('height', 0.5)

el.update = update
el.update({
...other
})
}

const update = function ({
primaryColor,
secondaryColor,
value
}) {
const svg = d3.select(this)
svg.node().dataset.value = value
svg.select('.knob-rotating')
.attr('transform', `rotate(${rotateScale(value)})`)

svg.select('.knob-big-circle')
.attr('fill', primaryColor)

svg.select('.knob-little-spot')
.attr('fill', secondaryColor)
}

/**
* Horizon Gauge
*/
export default class Knob extends React.PureComponent {
/**
* @override
*/
constructor (props) {
super(props)
this.elem = React.createRef()
this.mouseDown = this.mouseDown.bind(this)
this.mouseMove = this.mouseMove.bind(this)
this.mouseUp = this.mouseUp.bind(this)
this.touchStart = this.touchStart.bind(this)
this.touchMove = this.touchMove.bind(this)
this.touchEnd = this.touchEnd.bind(this)
}

/**
* @override
*/
componentDidMount () {
this.elem.current.addEventListener('touchstart', this.touchStart, { passive: false })
this.init()
}

componentWillUnmount () {
window.removeEventListener('mousemove', this.mouseMove)
window.removeEventListener('mouseup', this.mouseUp)
window.removeEventListener('touchmove', this.touchMove)
window.removeEventListener('touchend', this.touchEnd)
}

init () {
this.elem.current.innerHTML = ''
init(this.elem.current, this.props)
}

mouseDown (e) {
e.preventDefault()
this._initialValue = this.props.value
this._initialX = e.screenX
this._initialY = e.screenY
window.addEventListener('mousemove', this.mouseMove)
window.addEventListener('mouseup', this.mouseUp)
}

mouseMove (e) {
e.preventDefault()
const diff = e.screenX - this._initialX
const scaler = d3.scaleLinear([0, 100], [0, 1])
this.props.onValueChange(_.clamp(this._initialValue + scaler(diff), -1, 1))
}

mouseUp (e) {
window.removeEventListener('mousemove', this.mouseMove)
window.removeEventListener('mouseup', this.mouseUp)
}

touchStart (e) {
e.preventDefault()
this._initialValue = this.props.value
const touch = e.changedTouches[0]
this._initialX = touch.screenX
this._initialY = touch.screenY
this._touchEvent = touch.identifier
window.addEventListener('touchmove', this.touchMove)
window.addEventListener('touchend', this.touchEnd)
}

touchMove (e) {
const touch = _.find(e.changedTouches, (t) => t.identifier === this._touchEvent)
if (!touch) {
return
}
const diff = touch.screenX - this._initialX
const scaler = d3.scaleLinear([0, 100], [0, 1])
this.props.onValueChange(_.clamp(this._initialValue + scaler(diff), -1, 1))
}

touchEnd (e) {
window.removeEventListener('touchmove', this.touchMove)
window.removeEventListener('touchend', this.touchEnd)
}

/**
* @override
*/
componentDidUpdate (prevProps) {
const ignored = ['onValueChange']
const updatable = ['value', 'primaryColor', 'secondaryColor']

const p = _.omit(prevProps, ignored)
const n = _.omit(this.props, ignored)
if (!_.isEqual(_.omit(p, updatable), _.omit(n, updatable))) {
this.init()
} else {
this.elem.current.update(_.pick(n, updatable))
}
}

/**
* @override
*/
render () {
return (
<svg ref={this.elem} style={this.props.style} className={this.props.className}
onMouseDown={this.mouseDown}>
</svg>
)
}
}

Knob.propTypes = {
style: PropTypes.object,
className: PropTypes.string,
primaryColor: PropTypes.string,
secondaryColor: PropTypes.string,
value: PropTypes.number,
onValueChange: PropTypes.func
}

Knob.defaultProps = {
primaryColor: '#2a9fd6',
secondaryColor: '#fff',
value: 0,
onValueChange: () => {}
}

0 comments on commit 7c834aa

Please sign in to comment.