Last active
April 22, 2019 00:24
-
-
Save kefavn/fa565bb3715b8c8e035b88036cc21a11 to your computer and use it in GitHub Desktop.
WA - 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
export default Ember.Component.extend({ | |
tagName: '' | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@ember/component'; | |
import { action, computed } from '@ember-decorators/object'; | |
import { className, classNames } from '@ember-decorators/component'; | |
import { inject as service } from '@ember-decorators/service'; | |
@classNames('x-audio-out') | |
export default class extends Component { | |
@service audio | |
@className | |
@computed('audio.isPlaying') | |
get active() { | |
return this.audio.isPlaying; | |
} | |
didInsertElement() { | |
this.elementUpdated(this.element); | |
} | |
click() { | |
if (this.audio.isPlaying) { | |
return this.audio.pause(); | |
} | |
return this.audio.play(); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@ember/component'; | |
import EmberObject from '@ember/object'; | |
import { connectElements } from 'app/utils/svg-connect'; | |
import { computed, action } from '@ember-decorators/object'; | |
import { className, classNames } from '@ember-decorators/component'; | |
import { inject as service } from '@ember-decorators/service'; | |
@classNames('x-connector') | |
export default class extends Component { | |
constructor() { | |
super(...arguments); | |
this.stateMap = EmberObject.create({}); | |
this.stateElementMap = EmberObject.create({}); | |
} | |
didRender() { | |
this.updateSVG(); | |
} | |
updateSVG() { | |
const controls = this.output; | |
if (!this.output) { | |
return; | |
} | |
const container = $(this.element.querySelector('.svgContainer')); | |
const s = $(this.element.querySelector('.svg1')); | |
const $controls = $(this.output); | |
Object.keys(this.stateMap).forEach(label => { | |
const element = $(`#${this.elementId}${label}`); | |
if (element.length) { | |
const el = this.stateElementMap[label]; | |
connectElements(container, s, element, $(el), $controls); | |
} | |
}); | |
} | |
@action | |
addInput(element, label) { | |
this.set(`stateMap.${label}`, true); | |
this.set(`stateElementMap.${label}`, element); | |
} | |
@action | |
addOutput(element) { | |
this.set('output', element); | |
} | |
@action | |
updateState(label, state) { | |
this.set(`stateMap.${label}`, state); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@ember/component'; | |
import EmberObject from '@ember/object'; | |
import { connectElements } from 'app/utils/svg-connect'; | |
import { computed, action } from '@ember-decorators/object'; | |
import { className, classNames } from '@ember-decorators/component'; | |
import { inject as service } from '@ember-decorators/service'; | |
import { htmlSafe } from '@ember/template'; | |
class Key extends EmberObject { | |
static blackKeys = [1,3,6,8,10,13,15]; | |
static blackKeyOffsets = [1,2,4,5,6,8,9]; | |
active = false; | |
constructor({ offset, code }) { | |
super(...arguments); | |
this.offset = offset; | |
this.code = code; | |
} | |
get isBlack() { | |
return Key.blackKeys.indexOf(this.offset) >= 0; | |
} | |
get isWhite() { | |
return !this.isBlack; | |
} | |
get css() { | |
const keyOffset = Key.blackKeys.indexOf(this.offset); | |
const offset = Key.blackKeyOffsets[keyOffset]; | |
return htmlSafe(`grid-column-start: ${offset + 1}`); | |
} | |
destroy() { | |
} | |
} | |
@classNames('x-keyboard') | |
export default class extends Component { | |
@service keyboard; | |
@service audio; | |
_keys = [ | |
'KeyZ', | |
'KeyS', | |
'KeyX', | |
'KeyD', | |
'KeyC', | |
'KeyV', | |
'KeyG', | |
'KeyB', | |
'KeyH', | |
'KeyN', | |
'KeyJ', | |
'KeyM', | |
'Comma', | |
'KeyL', | |
'Period', | |
'Semicolon', | |
'Slash' | |
]; | |
constructor() { | |
super(...arguments); | |
this.keyboard.on('keyDown', (...args) => this.keyDown(...args)); | |
this.keyboard.on('keyUp', (...args) => this.keyUp(...args)); | |
} | |
@computed() | |
get keys() { | |
return this._keys.map((code, offset) => new Key({ code, offset })); | |
} | |
cleanupKeys() { | |
this.keys.forEach(key => key.destroy()); | |
} | |
keyDown(event) { | |
const { code } = event; | |
const key = this.keys.findBy('code', code); | |
if (!key) { | |
return; | |
} | |
key.set('active', true); | |
this.audio.trigger('noteDown', key.offset); | |
} | |
keyUp(event) { | |
const { code } = event; | |
const key = this.keys.findBy('code', code); | |
if (!key) { | |
return; | |
} | |
key.set('active', false); | |
this.audio.trigger('noteUp', key.offset); | |
} | |
focusOut() { | |
this.cleanupKeys(); | |
} | |
mouseDown() { | |
this.set('rollAllowed', true); | |
} | |
mouseUp() { | |
this.set('rollAllowed', false); | |
} | |
mouseEnter(event) { | |
if (event.buttons) { | |
console.log('roll'); | |
this.set('rollAllowed', true); | |
} | |
} | |
mouseLeave() { | |
this.set('rollAllowed', false); | |
} | |
@action | |
keyMouseEnter(key) { | |
if (!this.rollAllowed) { | |
return; | |
} | |
key.set('active', true); | |
this.audio.trigger('noteDown', key.offset); | |
} | |
@action | |
keyMouseLeave(key) { | |
console.log(`leave: ${key.code}`); | |
key.set('active', false); | |
this.audio.trigger('noteUp', key.offset); | |
} | |
@action | |
keyMouseDown(key, event) { | |
event.preventDefault(); | |
key.set('active', true); | |
this.audio.trigger('noteDown', key.offset); | |
} | |
@action | |
keyMouseUp(key, event) { | |
key.set('active', false); | |
this.audio.trigger('noteUp', key.offset); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import Component from '@ember/component'; | |
import { computed } from '@ember-decorators/object'; | |
import { className, classNames } from '@ember-decorators/component'; | |
import { htmlSafe } from '@ember/template'; | |
import { restartableTask } from 'ember-concurrency-decorators'; | |
import { debounce } from '@ember/runloop'; | |
export default class extends Component { | |
@className('active-knob') | |
_activeDrag = false; | |
constructor() { | |
super(...arguments); | |
this._prevValue = 0; | |
this.dragResistance = 3; | |
this.wheelResistance = 100; | |
this.integerOnly = true; | |
this._handlers = { | |
inputChange: this.handleInputChange.bind(this), | |
touchStart: this.handleTouchStart.bind(this), | |
touchMove: this.handleTouchMove.bind(this), | |
touchEnd: this.handleTouchEnd.bind(this), | |
touchCancel: this.handleTouchCancel.bind(this), | |
mouseDown: this.handleMouseDown.bind(this), | |
mouseMove: this.handleMouseMove.bind(this), | |
mouseUp: this.handleMouseUp.bind(this), | |
mouseWheel: this.handleMouseWheel.bind(this), | |
focus: this.handleFocus.bind(this), | |
blur: this.handleBlur.bind(this), | |
}; | |
} | |
didInsertElement() { | |
super.didInsertElement(...arguments); | |
this.element.addEventListener('wheel', this._handlers.mouseWheel); | |
} | |
@computed('knobOffset') | |
get gripStyle() { | |
const deg = -132 + this.knobOffset * 264; | |
return htmlSafe(`transform: translate(-50%, -50%) rotate(${deg}deg);`); | |
} | |
@computed('value', 'min', 'max') | |
get knobOffset() { | |
const min = this.min; | |
const max = this.max; | |
// console.log(`V: ${this.value} Mi: ${min} ma: ${max}`); | |
return Math.abs(this.value - this.min) / (this.max - this.min); | |
} | |
@computed('knobOffset') | |
get gripStrokeOffset() { | |
const offset = (1 - this.knobOffset) * 184; | |
return htmlSafe(`stroke-dashoffset: ${offset}`); | |
} | |
mouseDown(event) { | |
return this.handleMouseDown(event); | |
} | |
touchStart(event) { | |
return this.handleTouchStart(event); | |
} | |
updateValue(value) { | |
this.onChange(value); | |
} | |
handleMouseDown(evt) { | |
// console.log('mouse down'); | |
this.clearDrag(); | |
evt.preventDefault(); | |
this.set('_activeDrag', true); | |
this.startDrag(evt.clientY); | |
// drag update/end listeners | |
document.body.addEventListener('mousemove', this._handlers.mouseMove); | |
document.body.addEventListener('mouseup', this._handlers.mouseUp); | |
} | |
// handlers | |
handleInputChange(evt) { | |
// console.log('input change'); | |
this.updateToInputValue(); | |
} | |
handleTouchStart(evt) { | |
// console.log('touch start'); | |
this.clearDrag(); | |
evt.preventDefault(); | |
var touch = evt.changedTouches.item(evt.changedTouches.length - 1); | |
this.set('_activeDrag', touch.identifier); | |
this.startDrag(touch.clientY); | |
// drag update/end listeners | |
document.body.addEventListener('touchmove', this._handlers.touchMove); | |
document.body.addEventListener('touchend', this._handlers.touchEnd); | |
document.body.addEventListener('touchcancel', this._handlers.touchCancel); | |
} | |
handleTouchMove(evt) { | |
// console.log('touch move'); | |
var activeTouch = this.findActiveTouch(evt.changedTouches); | |
if (activeTouch) { | |
this.updateDrag(activeTouch.clientY); | |
} else if (!this.findActiveTouch(evt.touches)) { | |
this.clearDrag(); | |
} | |
} | |
handleTouchEnd(evt) { | |
// console.log('touch end'); | |
var activeTouch = this.findActiveTouch(evt.changedTouches); | |
if (activeTouch) { | |
this.finalizeDrag(activeTouch.clientY); | |
} | |
} | |
handleTouchCancel(evt) { | |
// console.log('touch cancel'); | |
if (this.findActiveTouch(evt.changedTouches)) { | |
this.clearDrag(); | |
} | |
} | |
handleMouseDown(evt) { | |
// console.log('mouse down'); | |
this.clearDrag(); | |
evt.preventDefault(); | |
this.set('_activeDrag', true); | |
this.startDrag(evt.clientY); | |
// drag update/end listeners | |
document.body.addEventListener('mousemove', this._handlers.mouseMove); | |
document.body.addEventListener('mouseup', this._handlers.mouseUp); | |
} | |
handleMouseMove(evt) { | |
// console.log('mouse move'); | |
if (evt.buttons&1) { | |
this.updateDrag(evt.clientY); | |
} else { | |
this.finalizeDrag(evt.clientY); | |
} | |
} | |
handleMouseUp(evt) { | |
// console.log('mouse up'); | |
this.finalizeDrag(evt.clientY); | |
} | |
handleMouseWheel(evt) { | |
// console.log('mouse wheel'); | |
this.element.focus(); | |
this.clearDrag(); | |
this._prevValue = parseFloat(this.value); | |
this.updateFromDrag(evt.deltaY, this.wheelResistance); | |
} | |
handleDoubleClick(evt) { | |
// console.log('double click'); | |
this.clearDrag(); | |
this.updateValue(this.initial); | |
this.updateToInputValue(); | |
} | |
handleFocus(evt) { | |
// console.log('focus on'); | |
//this._container.classList.add('focus-active'); | |
} | |
handleBlur(evt) { | |
// console.log('focus off'); | |
//this._container.classList.remove('focus-active'); | |
} | |
// dragging | |
startDrag(yPosition) { | |
this._dragStartPosition = yPosition; | |
this._prevValue = parseFloat(this.value); | |
this.element.focus(); | |
} | |
updateDrag(yPosition) { | |
var diff = yPosition - this._dragStartPosition; | |
this.updateFromDrag(diff, this.dragResistance); | |
} | |
finalizeDrag(yPosition) { | |
var diff = yPosition - this._dragStartPosition; | |
this.updateFromDrag(diff, this.dragResistance); | |
this.clearDrag(); | |
} | |
clearDrag() { | |
this.set('_activeDrag', false); | |
// clean up event listeners | |
document.body.removeEventListener('mousemove', this._handlers.mouseMove); | |
document.body.removeEventListener('mouseup', this._handlers.mouseUp); | |
document.body.removeEventListener('touchmove', this._handlers.touchMove); | |
document.body.removeEventListener('touchend', this._handlers.touchEnd); | |
document.body.removeEventListener('touchcancel', this._handlers.touchCancel); | |
} | |
updateToInputValue() { | |
var normVal = this.normalizeValue(parseFloat(this._input.value)); | |
this.updateVisuals(normVal); | |
} | |
updateFromDragTask(dragAmount, resistance) { | |
let value = this._prevValue - (dragAmount/resistance); | |
console.log(`DART: ${dragAmount} Res: ${resistance} V: ${value}`); | |
if (this.integerOnly) { | |
value = parseInt(value); | |
} | |
var clampedValue = this.clampValue(value); | |
this.updateValue(clampedValue); | |
} | |
updateFromDrag(dragAmount, resistance) { | |
//debounce(this, this.updateFromDragTask, dragAmount, resistance, 16); | |
this.updateFromDragTask(dragAmount, resistance); | |
} | |
// utils | |
clampValue(val) { | |
var min = parseFloat(this.min); | |
var max = parseFloat(this.max); | |
return Math.min(Math.max(val, min), max); | |
} | |
normalizeValue(val) { | |
var min = parseFloat(this.min); | |
var max = parseFloat(this.max); | |
return (val-min)/(max-min); | |
} | |
findActiveTouch(touchList) { | |
var i, len, touch; | |
for (i=0, len=touchList.length; i<len; i++) | |
if (this._activeDrag === touchList.item(i).identifier) | |
return touchList.item(i); | |
return null; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
import { inject as service } from '@ember/service'; | |
import { computed } from '@ember/object'; | |
import { Oscillator } from 'app/utils/audio'; | |
export default Ember.Component.extend({ | |
classNames: ['panel'], | |
active: true, | |
audio: service(), | |
init() { | |
this._super(...arguments); | |
this.oscillatorSettings = {}; | |
const oscillator = this.createOscillator(); | |
this.oscillator = oscillator; | |
if (this.startingNoteOffset) { | |
this.oscillator.changeNoteOffset(this.startingNoteOffset); | |
} | |
if (this.startingGain) { | |
this.oscillator.changeGain(this.startingGain); | |
} | |
if (this.startingWaveType) { | |
this.oscillator.changeWaveType(this.startingWaveType); | |
} | |
this.audio.on('noteDown', (...args) => this.noteDown(...args)); | |
this.audio.on('noteUp', (...args) => this.noteUp(...args)); | |
}, | |
createOscillator() { | |
const out = this.audio.master; | |
const audioContext = this.audio.audioContext; | |
const oscillator = new Oscillator({ out, audioContext }); | |
this.audio.addOscillator(oscillator); | |
return oscillator; | |
}, | |
didInsertElement() { | |
if (this.elementUpdated) { | |
this.elementUpdated(this.element.querySelector('.connectable'), this.label); | |
} | |
}, | |
noteDown(offset) { | |
console.trace(); | |
console.log(offset); | |
const oscillator = this.createOscillator(); | |
this.changeBaseNote(offset); | |
}, | |
noteUp(offset) { | |
//this.changeBaseNote(offset); | |
}, | |
changeBaseNote(offset) { | |
this.oscillator.changeBaseNote(offset); | |
}, | |
actions: { | |
changeNoteOffset(offset) { | |
this.oscillator.changeNoteOffset(offset); | |
}, | |
changeLowOscillatorType(type) { | |
this.oscillator.changeWaveType(type); | |
}, | |
changeGain(gain) { | |
this.oscillator.changeGain(gain); | |
}, | |
setActive() { | |
this.toggleProperty('active'); | |
if (this.audio.isPlaying) { | |
if (this.active) { | |
this.oscillator.on(); | |
} else { | |
this.oscillator.off(); | |
} | |
} | |
this.updateState(this.label, this.active); | |
} | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component from '@ember/component'; | |
import EmberObject from '@ember/object'; | |
import { connectElements } from 'app/utils/svg-connect'; | |
import { computed, action } from '@ember-decorators/object'; | |
import { className, classNames } from '@ember-decorators/component'; | |
import { inject as service } from '@ember-decorators/service'; | |
@classNames('x-wave-visualizer') | |
export default class extends Component { | |
@service audio | |
visualizerId = 'visualizer'; | |
didInsertElement() { | |
this.audio.oscilloscope.id = this.visualizerId; | |
this.audio.oscilloscope.draw(); | |
} | |
@computed('audio.analyser') | |
get visualizer() { | |
const analyser = this.audio.analyser; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Mixin from '@ember/object/mixin'; | |
import { inject as service } from '@ember-decorators/service'; | |
export default class AudioNode extends Mixin { | |
@service audio; | |
node = null; | |
input = null; | |
output = null; | |
group = null; | |
constructor() { | |
super(...arguments); | |
if (!this.node || !this.group) { | |
return console.error('Missing node or group'); | |
} | |
if (!this.input && !this.output) { | |
return console.error('Input/Output missing'); | |
} | |
this.group.nodes.addObject(this.node); | |
} | |
willDestroyElement() { | |
this.group.nodes.removeObject(this.node); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Service from '@ember/service'; | |
import { computed } from '@ember-decorators/object'; | |
import Evented from '@ember/object/evented'; | |
import { Note } from 'app/utils/frequency'; | |
import { LookaheadLimiter } from 'app/utils/custom-audio-nodes'; | |
import { OscilloscopeSecond } from 'app/utils/oscilloscope'; | |
export default class extends Service.extend(Evented) { | |
isPlaying = false; | |
oscillators = []; | |
groups = []; | |
tree = {}; | |
@computed() | |
get audioContext() { | |
return new (window.AudioContext || window.webkitAudioContext)(); | |
} | |
@computed('audioContext') | |
get masterCompressor() { | |
const audioContext = this.audioContext; | |
const compressor = this.audioContext.createDynamicsCompressor(); | |
compressor.threshold.setValueAtTime(5, audioContext.currentTime); | |
compressor.knee.setValueAtTime(30, audioContext.currentTime); | |
compressor.ratio.setValueAtTime(12, audioContext.currentTime); | |
compressor.attack.setValueAtTime(0, audioContext.currentTime); | |
compressor.release.setValueAtTime(0.25, audioContext.currentTime); | |
return compressor; | |
} | |
@computed('audioContext') | |
get lookAheadLimiter() { | |
const audioContext = this.audioContext; | |
return new LookaheadLimiter({ audioContext }); | |
} | |
@computed('audioContext') | |
get oscilloscope() { | |
const audioContext = this.audioContext; | |
return new OscilloscopeSecond({ audioContext }); | |
} | |
@computed('audioContext', 'masterCompressor') | |
get master() { | |
const compressor = this.masterCompressor; | |
const limiter = this.lookAheadLimiter; | |
const gainNode = this.audioContext.createGain(); | |
const oscilloscope = this.oscilloscope; | |
gainNode.gain.setValueAtTime(0.5, this.audioContext.currentTime); | |
limiter.node.connect(gainNode); | |
gainNode.connect(oscilloscope.node); | |
oscilloscope.node.connect(this.audioContext.destination); | |
return limiter.node; | |
} | |
addOscillator(oscillator, label, offset) { | |
this.oscillators.pushObject(oscillator); | |
if (label) { | |
if (!this.tree[label]) { | |
this.tree[label] = {}; | |
} | |
this.tree[label][offset] = oscillator; | |
}; | |
} | |
removeOscillator(label, offset) { | |
this.tree[label][offset].off(); | |
this.tree[label][offset].destroy(); | |
delete this.tree[label][offset]; | |
} | |
addGroup(group) { | |
} | |
play() { | |
this.set('isPlaying', true); | |
console.log(this.oscillators); | |
this.oscillators.forEach(oscillator => oscillator.on()); | |
} | |
pause() { | |
this.oscillators.forEach(oscillator => oscillator.off()); | |
this.set('isPlaying', false); | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Service from '@ember/service'; | |
import Evented from '@ember/object/evented'; | |
export default class extends Service.extend(Evented) { | |
constructor() { | |
super(...arguments); | |
this._handlers = { | |
keyDown: this.handleKeyDown.bind(this), | |
keyUp: this.handleKeyUp.bind(this) | |
}; | |
document.body.addEventListener('keydown', this._handlers.keyDown); | |
document.body.addEventListener('keyup', this._handlers.keyUp); | |
} | |
handleKeyDown(event) { | |
this.trigger('keyDown', event); | |
} | |
handleKeyUp(event) { | |
this.trigger('keyUp', event); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html { | |
--primary-color: #E4E8EA; | |
--background-color: #181B1C; | |
--color-dead: rgba(0,0,0,0.2); | |
--dark: rgba(0,0,0,0.3); | |
--darker: rgba(0,0,0,0.5); | |
} | |
body { | |
margin: 2em 2em; | |
font-family: monospace, Helvetica, Arial, sans-serif; | |
background-color: var(--background-color); | |
color: var(--primary-color); | |
} | |
button { | |
height: 24px; | |
} | |
.container { | |
margin: 2em 2em; | |
border-radius: 10px; | |
border-color: var(--primary-color); | |
border-width: 3px; | |
border-style: solid; | |
} | |
.container > h2 { | |
font-weight: 400; | |
padding-left: 30px; | |
padding-right: 30px; | |
margin-top: 0px; | |
transform: translate(0, -60%); | |
margin-bottom: 10px; | |
background-color: var(--background-color); | |
} | |
.section { | |
padding-left: 20px; | |
padding-right: 20px; | |
width: 100%; | |
} | |
.grid { | |
display: grid; | |
grid-template-rows: repeat(1, 480px); | |
grid-template-columns: repeat(3, 140px); | |
grid-column-gap: 20px; | |
grid-row-gap: 20px; | |
} | |
.grid-item { | |
z-index: 2; | |
grid-column-start: 1; | |
grid-column-end: 10; | |
grid-row-start: 1; | |
grid-row-end: 10; | |
} | |
.item-container { | |
width: 300px; | |
height: 300px; | |
border-radius: 5px; | |
border: 3px dashed #999; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.color-dead { | |
--highlight-color: var(--color-dead); | |
} | |
.white { | |
--highlight-color: white; | |
} | |
.orange { | |
--highlight-color: hsl(32, 95%, 50%); | |
} | |
.green { | |
--highlight-color: hsl(144, 95%, 38%); | |
} | |
.pink { | |
--highlight-color: hsl(313, 95%, 50%); | |
} | |
.blue { | |
--highlight-color: hsl(240, 95%, 50%); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.page { | |
display: block; | |
position: relative; | |
} | |
.center { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
flex-direction: column; | |
} | |
.center.row { | |
flex-direction: row; | |
} | |
.inline.center { | |
display: inline-flex; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.x-audio-out { | |
font-size: 48px; | |
border-radius: 50%; | |
text-align: center; | |
line-height: 80px; | |
width: 80px; | |
height: 80px; | |
opacity: 0.5; | |
user-select: none; | |
cursor: pointer; | |
background-color: var(--darker); | |
transition: 0.3s cubic-bezier(0, 0, 0.24, 1); | |
border: 2px solid var(--primary-color); | |
} | |
.x-audio-out.active { | |
opacity: 1; | |
background-color: var(--highlight-color, var(--primary-color)); | |
transition: 0s; | |
animation: pulse-outer 2s linear infinite; | |
} | |
@keyframes pulse-outer { | |
0% { | |
box-shadow: 0 0 0 2px rgba(228,232,234, 0) | |
} | |
40% { | |
box-shadow: 0 0 0 4px rgba(228,232,234, 0.2) | |
} | |
70% { | |
box-shadow: 0 0 0 10px rgba(228,232,234, 0.12) | |
} | |
100% { | |
box-shadow: 0 0 0 12px rgba(228,232,234, 0) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.svgContainer { | |
z-index: -10; | |
position:absolute; | |
top: 0; | |
left: 0; | |
} | |
.svgContainer path { | |
stroke: white; | |
stroke-dashoffset: 100%; | |
stroke-dasharray: 100% 100%; | |
opacity: 0.25; | |
transition: 0.4s cubic-bezier(0, 0, 0.24, 1); | |
} | |
.svgContainer path.on { | |
stroke-dashoffset: 0%; | |
stroke: var(--primary-color); | |
opacity: 0.5; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.x-keyboard { | |
position: relative; | |
} | |
.x-keyboard-track { | |
display: grid; | |
grid-template-columns: repeat(10, 13px); | |
grid-template-rows: 12px 60px; | |
grid-gap: 1px; | |
background-color: hsl(0,0%,60%); | |
border-radius: 5px; | |
} | |
.x-keyboard-track--upper { | |
position: absolute; | |
top: 0; | |
background-color: rgba(0,0,0,0); | |
pointer-events: none; | |
border-radius: 5px; | |
} | |
.x-header { | |
border-top-left-radius: 4px; | |
border-top-right-radius: 4px; | |
background-color: hsl(0,0%,80%); | |
box-shadow: inset 0px -1px 2px rgba(255,255,255,0.4),0 1px 3px rgba(0,0,0,0.4); | |
grid-column-start: 1; | |
grid-column-end: span 10; | |
border-bottom: 1px solid; | |
border-bottom-color: hsl(0,0%,60%); | |
z-index: 1; | |
} | |
.x-key { | |
background-color: hsl(0,0%,80%); | |
border-radius: 2px; | |
margin-top: -2px; | |
} | |
.x-key.active { | |
background-color: hsl(0,0%,70%); | |
} | |
.x-placeholder { | |
pointer-events: none; | |
background-color: rgba(0,0,0,0); | |
} | |
.x-key.black { | |
box-shadow: inset 0px -1px 2px rgba(255,255,255,0.4),0 2px 3px rgba(0,0,0,0.4); | |
background-color: hsl(0,0%,10%); | |
height: 65%; | |
width: 6px; | |
border: 1px solid hsl(0,0%,10%); | |
transform: translate(-55%); | |
pointer-events: initial; | |
} | |
.x-key.black.active { | |
border: 1px solid hsl(0,0%,40%); | |
background-color: hsl(0,0%,50%); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.knob-container { | |
margin: 10px; | |
color: var(--highlight-color, white); | |
position: relative; | |
width: 100px; | |
height: 100px; | |
} | |
.knob-handle svg { | |
pointer-events: none; | |
position: absolute; | |
stroke-width: 8; | |
stroke-dasharray: 184 184; | |
stroke-width: 5; | |
stroke-color: currentColor; | |
} | |
.knob-handle svg path { | |
transition: 0.3s cubic-bezier(0, 0, 0.24, 1); | |
} | |
.knob-handle svg path.colored { | |
stroke: var(--highlight-color, white); | |
} | |
.knob-handle svg path.track { | |
stroke: rgba(255,255,255, 0.1); | |
} | |
.knob-grip { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
border-radius: 50%; | |
z-index: 5; | |
height: 82px; | |
width: 82px; | |
transition: 0.3s cubic-bezier(0, 0, 0.24, 1); | |
} | |
.active-knob .knob-grip { | |
width: 96px; | |
height: 96px; | |
} | |
.knob-grip::after { | |
height: 20px; | |
width: 2px; | |
border-radius: 4px; | |
background-color: var(--highlight-color, white); | |
content: ""; | |
position: absolute; | |
top: -1px; | |
left: 50%; | |
right: ; | |
bottom: ; | |
transform: translateX(-50%); | |
} | |
.knob-value { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
margin-top: -3px; | |
transform: translate(-50% , -50%); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.inner-panel .panel-title { | |
padding-bottom: 10px; | |
margin-bottom: 0px; | |
user-select: none; | |
cursor: pointer; | |
} | |
.inner-panel { | |
position: relative; | |
border-radius: 3px; | |
background-color: #2c2d2f; | |
} | |
.inner-panel > * { | |
width: 100%; | |
margin: 10px; | |
margin-top: 0px; | |
} | |
.inner-panel .panel-body, | |
.inner-panel .panel-title { | |
margin-top: 10px; | |
} | |
.inner-panel h4 { | |
border-bottom: 4px solid #181b1c; | |
} | |
.inner-panel .panel-row { | |
margin-bottom: 10px; | |
margin-top: 10px; | |
} | |
.panel-state { | |
position: absolute; | |
top: 5px; | |
left: 5px; | |
border-radius: 50%; | |
width: 8px; | |
height: 8px; | |
background-color: rgba(0,0,0,0.3); | |
} | |
.panel-state.active { | |
background-color: var(--highlight-color, var(--primary-color)); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": "0.15.1", | |
"EmberENV": { | |
"FEATURES": {} | |
}, | |
"options": { | |
"use_pods": false, | |
"enable-testing": false | |
}, | |
"dependencies": { | |
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js", | |
"ember": "3.4.3", | |
"ember-template-compiler": "3.4.3", | |
"ember-testing": "3.4.3" | |
}, | |
"addons": { | |
"ember-data": "3.4.2", | |
"ember-decorators": "2.0.0", | |
"ember-truth-helpers": "2.1.0", | |
"ember-contextual-states": "0.1.6", | |
"ember-animated": "0.5.1", | |
"ember-decorators": "5.2.0", | |
"ember-concurrency": "0.9.0", | |
"ember-concurrency-decorators": "0.6.0" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class { | |
nodes = []; | |
addNode(node) { | |
this.nodes.push(node); | |
} | |
start() { | |
this.nodes.forEach(node => node.start()); | |
} | |
stop() { | |
this.nodes.forEach(node => node.stop()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Note } from 'app/utils/frequency'; | |
import { computed } from '@ember-decorators/object'; | |
import EmberObject from '@ember/object'; | |
export class Oscillator extends EmberObject { | |
startedBefore = false; | |
active = false; | |
oscillatorNode = null; | |
waveType = 'sine'; | |
out = null; | |
audioContext = null; | |
baseNote = 0; | |
noteOffset = 0; | |
@computed('baseNote', 'noteOffset') | |
get note() { | |
return new Note(parseInt(this.baseNote) + parseInt(this.noteOffset)); | |
} | |
@computed('note') | |
get frequency() { | |
return this.note.frequency; | |
} | |
constructor({ gain, waveType, out, audioContext }) { | |
super(...arguments); | |
this.gain = gain || 50; | |
this.waveType = waveType || 'sine'; | |
this.out = out; | |
this.audioContext = audioContext; | |
this.setupOscillator(); | |
} | |
setupOscillator() { | |
this.oscillatorNode = this.createOscillatorNode(); | |
this.gainNode = this.createGainNode(); | |
this.changeGain(this.gain); | |
this.oscillatorNode.connect(this.gainNode); | |
this.gainNode.connect(this.out); | |
} | |
createGainNode() { | |
return this.audioContext.createGain(); | |
} | |
createOscillatorNode() { | |
const oscillator = this.audioContext.createOscillator(); | |
oscillator.type = this.waveType; | |
oscillator.frequency.setValueAtTime(this.frequency, this.audioContext.currentTime); | |
return oscillator; | |
} | |
syncFrequency() { | |
this.oscillatorNode.frequency.exponentialRampToValueAtTime(this.frequency, this.audioContext.currentTime + 0.0005); | |
} | |
changeWaveType(type) { | |
this.set('waveType', type); | |
console.log(type); | |
if (this.active) { | |
this.off(); | |
this.on(); | |
} else { | |
this.setupOscillator(); | |
} | |
} | |
changeBaseNote(offset) { | |
this.set('baseNote', offset); | |
this.syncFrequency(); | |
} | |
changeNoteOffset(offset) { | |
this.set('noteOffset', offset); | |
this.syncFrequency(); | |
} | |
changeGain(gain) { | |
this.set('gain', gain); | |
this.gainNode.gain.value = gain / 100; | |
} | |
on() { | |
this.active = true; | |
this.oscillatorNode.start(); | |
} | |
off() { | |
this.oscillatorNode.stop(); | |
this.gainNode.disconnect(); | |
this.oscillatorNode.disconnect(); | |
this.setupOscillator(); | |
this.active = false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var sampleRate = 44100; // Hz | |
var preGain = 0; //db | |
var postGain = 0; //db | |
var attackTime = 0; //s | |
var releaseTime = 0.5; //s | |
var threshold = -2; //dB | |
var lookAheadTime = 0.005; //s 5ms hard-coded | |
var delayBuffer = new DelayBuffer(lookAheadTime * sampleRate); | |
function DelayBuffer(n) { | |
n = Math.floor(n); | |
this._array = new Float32Array(2 * n); | |
this.length = this._array.length; // can be optimized! | |
this.readPointer = 0; | |
this.writePointer = n - 1; | |
for (var i=0; i<this.length; i++){ | |
this._array[i] = 0; | |
} | |
} | |
DelayBuffer.prototype.read = function() { | |
var value = this._array[this.readPointer % this.length]; | |
this.readPointer++; | |
return value; | |
}; | |
DelayBuffer.prototype.push = function(v) { | |
this._array[this.writePointer % this.length] = v; | |
this.writePointer++; | |
}; | |
var envelopeSample = 0; | |
var getEnvelope = function(data, attackTime, releaseTime, sampleRate){ | |
//attack and release in milliseconds | |
var attackGain = Math.exp(-1/(sampleRate*attackTime)); | |
var releaseGain = Math.exp(-1/(sampleRate*releaseTime)); | |
var envelope = new Float32Array(data.length); | |
for (var i=0; i < data.length; i++){ | |
var envIn = Math.abs(data[i]); | |
if (envelopeSample < envIn){ | |
envelopeSample = envIn + attackGain * (envelopeSample - envIn); | |
} | |
else { | |
envelopeSample = envIn + releaseGain * (envelopeSample - envIn); | |
} | |
envelope[i] = envelopeSample; | |
} | |
return envelope; | |
} | |
var ampToDB = function(value){ | |
return 20 * Math.log10(value); | |
} | |
var dBToAmp = function(db){ | |
return Math.pow(10, db/20); | |
} | |
export function limit(audioProcessingEvent){ | |
var inp = audioProcessingEvent.inputBuffer.getChannelData(0); | |
var out = audioProcessingEvent.outputBuffer.getChannelData(0); | |
//transform db to amplitude value | |
var postGainAmp = dBToAmp(postGain); | |
//apply pre gain to signal | |
var preGainAmp = dBToAmp(preGain); | |
for (var k=0; k < inp.length; ++k){ | |
out[k] = preGainAmp * inp[k]; | |
} | |
var envelopeData = getEnvelope(out, attackTime, releaseTime, sampleRate); | |
if (lookAheadTime > 0){ | |
//write signal into buffer and read delayed signal | |
for (var i = 0; i < out.length; i++){ | |
delayBuffer.push(out[i]); | |
out[i] = delayBuffer.read(); | |
} | |
} | |
//limiter mode: slope is 1 | |
var slope = 1; | |
for (var i=0; i<inp.length; i++){ | |
var gainDB = slope * (threshold - ampToDB(envelopeData[i])); | |
//is gain below zero? | |
gainDB = Math.min(0, gainDB); | |
var gain = dBToAmp(gainDB); | |
out[i] *= (gain * postGainAmp); | |
} | |
} | |
export class LookaheadLimiter { | |
constructor({ audioContext }) { | |
this.audioContext = audioContext; | |
const limiter = audioContext.createScriptProcessor(4096, 1, 1); | |
limiter.onaudioprocess = limit; | |
this.node = limiter; | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const LETTERS = ['A', 'A½', 'B', 'C', 'C½', 'D', 'D½', 'E', 'F', 'F½', 'G', 'G½']; | |
export class Note { | |
constructor(offsetFromMiddleA) { | |
this.noteOffset = parseInt(offsetFromMiddleA); | |
} | |
get frequency() { | |
return 440 * Math.pow(2, this.noteOffset/12); | |
} | |
get frequencyDisplay() { | |
return this.frequency.toFixed(2); | |
} | |
get octave() { | |
return 4 + parseInt(this.noteOffset/12); | |
} | |
get letterOffset() { | |
return (120 +this.noteOffset + 120) % 12; | |
} | |
get letter() { | |
return LETTERS[this.letterOffset]; | |
} | |
toString() { | |
return `${this.letter}${String.fromCharCode(this.octave + 8320)}`; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var g = function(id){ | |
return document.getElementById(id); | |
} | |
function draw(analyser, canvas_id) { | |
var width = g(canvas_id).width; | |
var height = g(canvas_id).height; | |
var context = g(canvas_id).getContext('2d'); | |
var data = new Uint8Array(width); | |
analyser.getByteTimeDomainData(data); | |
context.strokeStyle = "red"; | |
context.lineWidth = 2; | |
context.clearRect(0,0,width,height); | |
context.beginPath(); | |
context.strokeStyle = "hsl(240,30%,50%)"; | |
context.moveTo(0,height/2); | |
context.lineTo(width,height/2); | |
context.stroke(); | |
context.strokeStyle = "hsl(240,30%,80%)"; | |
var c = ((width / (analyser.fftSize/2) ) ); | |
context.beginPath(); | |
var zeroCross = findFirstPositiveZeroCrossing(data, width, height); | |
if (zeroCross==0) | |
zeroCross=1; | |
for (var i=zeroCross, j=0; j<(width-zeroCross); i++, j++) | |
context.lineTo(j,height-( c *data[i])); | |
context.stroke(); | |
requestAnimationFrame( function(a1,a2) { | |
return function() { draw(a1, a2); } | |
}(analyser, canvas_id) ); | |
} | |
var MINVAL = 134; // height/2 == zero. MINVAL is the "minimum detected signal" level. | |
function findFirstPositiveZeroCrossing(buf, buflen, height) { | |
var i = 0; | |
var last_zero = -1; | |
var t; | |
// advance until we're zero or negative | |
while (i<buflen && (buf[i] > height/2 ) ) | |
i++; | |
if (i>=buflen) | |
return 0; | |
// advance until we're above MINVAL, keeping track of last zero. | |
while (i<buflen && ((t=buf[i]) < MINVAL )) { | |
if (t >= height/2) { | |
if (last_zero == -1) | |
last_zero = i; | |
} else | |
last_zero = -1; | |
i++; | |
} | |
// we may have jumped over MINVAL in one sample. | |
if (last_zero == -1) | |
last_zero = i; | |
if (i==buflen) // We didn't find any positive zero crossings | |
return 0; | |
// The first sample might be a zero. If so, return it. | |
if (last_zero == 0) | |
return 0; | |
return last_zero; | |
} | |
var myBuffer = null; | |
function drawScope(analyser, ctx) { | |
var width = ctx.canvas.width; | |
var height = ctx.canvas.height; | |
var timeData = new Uint8Array(analyser.frequencyBinCount); | |
var scaling = height / 256; | |
var risingEdge = 0; | |
var edgeThreshold = 5; | |
analyser.getByteTimeDomainData(timeData); | |
ctx.fillStyle = 'rgba(0, 20, 0, 0.1)'; | |
ctx.fillStyle = "hsl(240,30%,20%)"; | |
ctx.fillRect(0, 0, width, height); | |
ctx.lineWidth = 2; | |
ctx.strokeStyle = "hsl(240,30%,80%)"; | |
ctx.beginPath(); | |
// No buffer overrun protection | |
while (timeData[risingEdge++] - 128 > 0 && risingEdge <= width); | |
if (risingEdge >= width) risingEdge = 0; | |
while (timeData[risingEdge++] - 128 < edgeThreshold && risingEdge <= width); | |
if (risingEdge >= width) risingEdge = 0; | |
for (var x = risingEdge; x < timeData.length && x - risingEdge < width; x++) | |
ctx.lineTo(x - risingEdge, height - timeData[x] * scaling); | |
ctx.stroke(); | |
} | |
export class Oscilloscope { | |
constructor({ audioContext }) { | |
this.audioContext = audioContext; | |
var analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 1024; | |
this.node = analyser; | |
} | |
draw(id) { | |
draw(this.node, id); | |
} | |
} | |
export class OscilloscopeSecond { | |
constructor({ audioContext }) { | |
this.audioContext = audioContext; | |
var analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 2048; | |
this.node = analyser; | |
} | |
draw() { | |
const id = this.id; | |
const scopeCtx = document.getElementById(id).getContext('2d'); | |
drawScope(this.node, scopeCtx); | |
requestAnimationFrame(this.draw.bind(this)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function signum(x) { | |
return (x < 0) ? -1 : 1; | |
} | |
function absolute(x) { | |
return (x < 0) ? -x : x; | |
} | |
function drawPath(svg, path, startX, startY, endX, endY) { | |
// get the path's stroke width (if one wanted to be really precize, one could use half the stroke size) | |
var stroke = parseFloat(path.attr("stroke-width")); | |
// check if the svg is big enough to draw the path, if not, set heigh/width | |
if (svg.attr("height") < endY) svg.attr("height", endY); | |
if (svg.attr("width" ) < (startX + stroke) ) svg.attr("width", (startX + stroke)); | |
if (svg.attr("width" ) < (endX + stroke) ) svg.attr("width", (endX + stroke)); | |
var deltaX = (endX - startX) * 0.15; | |
var deltaY = (endY - startY) * 0.15; | |
// for further calculations which ever is the shortest distance | |
var delta = deltaY < absolute(deltaX) ? deltaY : absolute(deltaX); | |
// set sweep-flag (counter/clock-wise) | |
// if start element is closer to the left edge, | |
// draw the first arc counter-clockwise, and the second one clock-wise | |
var arc1 = 0; var arc2 = 1; | |
if (startX > endX) { | |
arc1 = 1; | |
arc2 = 0; | |
} | |
// draw tha pipe-like path | |
// 1. move a bit down, 2. arch, 3. move a bit to the right, 4.arch, 5. move down to the end | |
path.attr("d", "M" + startX + " " + startY + | |
" V" + (startY + delta) + | |
" A" + delta + " " + delta + " 0 0 " + arc1 + " " + (startX + delta*signum(deltaX)) + " " + (startY + 2*delta) + | |
" H" + (endX - delta*signum(deltaX)) + | |
" A" + delta + " " + delta + " 0 0 " + arc2 + " " + endX + " " + (startY + 3*delta) + | |
" V" + endY ); | |
} | |
export function connectElements(svgContainer, svg, path, startElem, endElem) { | |
//console.log(`c: ${svgContainer} s: ${svg} p: ${path} se ${startElem} e ${endElem}`); | |
// if first element is lower than the second, swap! | |
if(startElem.offset().top > endElem.offset().top){ | |
var temp = startElem; | |
startElem = endElem; | |
endElem = temp; | |
} | |
// get (top, left) corner coordinates of the svg container | |
var svgTop = svgContainer.offset().top; | |
var svgLeft = svgContainer.offset().left; | |
// get (top, left) coordinates for the two elements | |
var startCoord = startElem.offset(); | |
var endCoord = endElem.offset(); | |
// calculate path's start (x,y) coords | |
// we want the x coordinate to visually result in the element's mid point | |
var startX = startCoord.left + 0.5*startElem.outerWidth() - svgLeft; // x = left offset + 0.5*width - svg's left offset | |
var startY = startCoord.top + startElem.outerHeight() - svgTop; // y = top offset + height - svg's top offset | |
// calculate path's end (x,y) coords | |
var endX = endCoord.left + 0.5*endElem.outerWidth() - svgLeft; | |
var endY = endCoord.top - svgTop; | |
// call function for drawing the path | |
drawPath(svg, path, startX, startY, endX, endY); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment