Skip to content

Instantly share code, notes, and snippets.

@kefavn
Last active April 22, 2019 00:24
Show Gist options
  • Save kefavn/fa565bb3715b8c8e035b88036cc21a11 to your computer and use it in GitHub Desktop.
Save kefavn/fa565bb3715b8c8e035b88036cc21a11 to your computer and use it in GitHub Desktop.
WA - 2
import Ember from 'ember';
export default Ember.Component.extend({
tagName: ''
});
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();
}
};
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);
}
};
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);
}
};
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;
}
};
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);
}
}
});
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;
}
};
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);
}
};
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);
}
};
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);
}
}
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;
}
.control-button {
font-weight: 400;
margin-left: 4px;
margin-right: 4px;
width: 24px;
height: 24px;
background-color: var(--dark);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: 1px rgba(0,0,0,0) solid;
cursor: pointer;
user-select: none;
border-radius: 3px;
}
.control-button.smallest {
font-size: 8px;
}
.control-button.small {
font-size: 14px;
}
.control-button.active {
background-color: var(--highlight-color, rgba(255,255,255,0.5));
}
.control-button:hover {
border: 1px var(--primary-color) solid;
}
.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%);
}
.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;
}
.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)
}
}
.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;
}
.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%);
}
.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%);
}
.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));
}
<div class="page">
<X-connector as |connector|>
<div class="container center inline">
<h2>
Web Audio
</h2>
<div class="section center">
<div class="grid">
<A-group as |group|>
<X-panel
@label="Low"
@group=group
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingNoteOffset=-12
@startingGain=100
class="c--osc-1 pink"/>
<X-panel
@label="Medium"
@group=group
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingGain=50
class="c--osc-2 green"/>
<X-panel
@label="High"
@group=group
@elementUpdated={{action connector.actions.addInput}}
@updateState={{action connector.actions.updateState}}
@startingWaveType='triangle'
@startingNoteOffset=12
@startingGain=20
class="c--osc-3 orange"/>
</A-group>
</div>
</div>
</div>
<X-audio-out @elementUpdated={{action connector.actions.addOutput}}/>
<div style="margin: 20px">
<X-keyboard/>
</div>
<div style="margin: 20px">
<X-wave-visualizer/>
</div>
</X-connector/>
</div>
<div class="center">
{{yield (hash
actions=(hash
addInput=(action 'addInput')
addOutput=(action 'addOutput')
updateState=(action 'updateState')
)
)}}
</div>
<div class="svgContainer" style="margin: 50px 50px;">
<svg class="svg1" width="0" height="0" >
{{#each-in stateElementMap as |label|}}
<path
id={{concat this.elementId label}}
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap label) 'on'}}"
/>
{{/each-in}}
<path
id="path2"
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap "Medium") 'on'}}"
/>
<path
id="path3"
d="M0 0"
fill="none"
stroke-width="4px"
class="{{if (get stateMap "High") 'on'}}"
/>
</svg>
</div>
<div class="x-keyboard-track">
<div class="x-header"></div>
{{#each keys as |key index|}}
{{#if key.isWhite}}
<div class="x-key {{if key.active 'active'}}" onmousedown={{action keyMouseDown key}} onmouseup={{action keyMouseUp key}} onmouseenter={{action keyMouseEnter key}} onmouseleave={{action keyMouseLeave key}}></div>
{{/if}}
{{/each}}
</div>
<div class="x-keyboard-track x-keyboard-track--upper">
<div class="x-header"></div>
{{#each keys as |key index|}}
{{#if key.isBlack}}
<div class="x-key black {{if key.active 'active'}}" style={{key.css}} onmousedown={{action keyMouseDown key}} onmouseenter={{action keyMouseEnter key}} onmouseup={{action keyMouseUp key}} onmouseleave={{action keyMouseUp key}}></div>
{{/if}}
{{/each}}
</div>
<div class="knob-container">
<div class="knob-track"></div>
<div class="knob-base">
</div>
<div class="knob-handle">
<svg width=100 height=100 viewBox="0 0 100 100" class="dial-svg">
<path class="track" d="M20,76 A 40 40 0 1 1 80 76" fill="none"/>
<path class="colored" d="M20,76 A 40 40 0 1 1 80 76" fill="none" style={{gripStrokeOffset}}/>
</svg>
</div>
<div class="knob-grip" style={{gripStyle}}></div>
<div class="knob-value">
{{@displayValue}}
</div>
</div>
<div class="center inner-panel {{if (not active) 'color-dead'}} connectable">
<h4 class="center panel-title" onclick={{action 'setActive'}}>
<div class="panel-state {{if active 'active'}}">
</div>
{{@label}}
</h4>
<div class="panel-body">
<div class="center">
<span>
Type
</span>
<div class="panel-row">
<div class="center row">
<div class="control-button {{if (eq oscillator.waveType 'sine') 'active'}}" {{action 'changeLowOscillatorType' 'sine'}}>
</div>
<div class="control-button small {{if (eq oscillator.waveType 'square') 'active'}}" {{action 'changeLowOscillatorType' 'square'}}>
</div>
<div class="control-button smallest {{if (eq oscillator.waveType 'sawtooth') 'active'}}" {{action 'changeLowOscillatorType' 'sawtooth'}}>
Saw
</div>
<div class="control-button small {{if (eq oscillator.waveType 'triangle') 'active'}}" {{action 'changeLowOscillatorType' 'triangle'}}>
</div>
</div>
</div>
</div>
<div class="center">
<span>
Frequency
</span>
<X-knob
@min=-48
@max=48
@value={{oscillator.noteOffset}}
@displayValue={{oscillator.noteOffset}}
@onChange={{action 'changeNoteOffset'}}
/>
</div>
<div class="center {{if active 'white' 'color-dead'}}">
<span>
Gain
</span>
<X-knob
@min=0
@max=100
@value={{oscillator.gain}}
@displayValue={{oscillator.gain}}
@onChange={{action 'changeGain'}}
/>
</div>
</div>
</div>
<canvas id={{visualizerId}} width="200" height="100" style="background-color: hsl(240, 30%, 20%); width: 200px; height: 100px; margin: 30px 0px;"></canvas>
{
"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"
}
}
export default class {
nodes = [];
addNode(node) {
this.nodes.push(node);
}
start() {
this.nodes.forEach(node => node.start());
}
stop() {
this.nodes.forEach(node => node.stop());
}
}
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;
}
}
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;
}
}
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)}`;
}
}
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));
}
}
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