Skip to content

Instantly share code, notes, and snippets.

@HelloWorld017
Last active September 17, 2017 04:56
Show Gist options
  • Select an option

  • Save HelloWorld017/bdefb8c745a29d780f0d934462120b07 to your computer and use it in GitHub Desktop.

Select an option

Save HelloWorld017/bdefb8c745a29d780f0d934462120b07 to your computer and use it in GitHub Desktop.
Oscillator-Oscilloscope
<!DOCTYPE html>
<html>
<head>
<title>Oscillator-Oscilloscope</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css">
<link rel="stylesheet" href="https://cdn.rawgit.com/HelloWorld017/NanumBarunGothic/master/nanumbarungothic.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, intial-scale=2, user-scalable=no">
<style>
.range-box {
display: flex;
}
.range-box>.range-field {
flex: 1;
margin: 10px;
display: flex;
}
body.dark {
background: #222;
}
body.dark h3, body.dark input {
color: #f1f2f3;
}
body.dark input:disabled, body.dark input:disabled+label {
color: rgba(255, 255, 255, 0.42) !important;
}
body.dark input:disabled {
border-bottom: 1px dotted rgba(255, 255, 255, 255.42) !important;
}
body.dark .modal {
background: #303030;
}
body.dark .modal-footer {
background: #353535;
}
body.dark .modal-footer .modal-action {
color: #f1f2f3;
}
h3 {
font-size: 1.5rem;
font-weight: 200;
}
* {
font-family: 'Nanum Barun Gothic', sans-serif;
}
*::-webkit-scrollbar-track {
background-color: transparent;
}
*::-webkit-scrollbar {
width: 20px;
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: #009688;
background-image: -webkit-linear-gradient(45deg,
#4db6ac 25%,
transparent 25%,
transparent 50%,
#4db6ac 50%,
#4db6ac 75%,
transparent 75%,
transparent)
}
body {
display: flex;
flex-direction: column;
height: 100vh;
}
main {
padding: 30px;
display: flex;
width: 100%;
flex: 1;
box-sizing: border-box;
}
aside {
display: flex;
flex: 1;
flex-direction: column;
padding-left: 30px;
justify-content: center;
}
form {
padding-right: 30px;
}
.controller {
flex: 2;
max-width: 768px;
overflow: auto;
}
input[type=range] {
border: none;
}
.play{
display: flex;
justify-content: center;
margin-top: 50px;
}
.play button {
width: 30%;
min-width: 100px;
height: 50px;
margin: 10px;
}
canvas {
width: 100%;
height: 300px;
}
p {
color: #f1f2f3;
text-align: center;
}
*::selection {
background: #009688;
}
@media (max-width: 1150px) {
.controller {
overflow: initial;
max-width: initial;
flex: initial;
}
body {
height: initial;
}
aside {
flex: initial;
display: block;
}
canvas {
height: 400px;
}
main {
flex-direction: column;
}
form {
padding: 0;
}
}
</style>
</head>
<body class="dark">
<nav>
<div class="teal nav-wrapper">
<div class="container">
<span class="brand-logo">Oscillator-Oscilloscope</span>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li><a href="https://gist.github.com/HelloWorld017/bdefb8c745a29d780f0d934462120b07">소스 보기</a></li>
<li><a href="https://khinenw.tk">개발자 정보</a></li>
</ul>
</div>
</div>
</nav>
<main>
<section class="controller">
<form>
<section class="section shape">
<h3>파형</h3>
<p>
<input name="shape" type="radio" id="sine" value="sine" checked>
<label for="sine">사인파</label>
</p>
<p>
<input name="shape" type="radio" id="square" value="square">
<label for="square">사각파</label>
</p>
<p>
<input name="shape" type="radio" id="sawtooth" value="sawtooth">
<label for="sawtooth">톱니파</label>
</p>
<p>
<input name="shape" type="radio" id="triangle" value="triangle">
<label for="triangle">삼각파</label>
</p>
<p>
<input name="shape" type="radio" id="custom" value="custom">
<label for="custom">푸리에 급수</label>
<br><br>
<div class="input-field fourier-text">
<input
id="custom-text-real"
class="validate"
type="text"
value="0, 0, 0, 0, 0"
disabled>
<label
data-error="실수부와 허수부의 크기가 같지 않거나 숫자와 컴마로 이루어져 있지 않습니다."
for="custom-text-real">
푸리에 급수 실수부 (a_0, a_1, a_2, ...)
</label>
</div>
<div class="input-field fourier-text">
<input
id="custom-text-imag"
class="validate"
type="text"
value="0, 1, 0, 0, 0"
disabled>
<label
data-error="실수부와 허수부의 크기가 같지 않거나 숫자와 컴마로 이루어져 있지 않습니다."
for="custom-text-imag">
푸리에 급수 허수부 (0, b_1, b_2, b_3, ...)
</label>
</div>
</p>
</section>
<section class="section frequency">
<h3>진동수</h3>
<div class="range-box">
<p class="range-field">
<input type="range" id="freq" min="1" max="24000" value="2000"/>
</p>
<div class="input-field inline">
<input id="freq-text" type="number" value="2000">
<label for="freq-text">진동수 (Hz)</label>
</div>
<a class="btn waves-effect waves-light modal-trigger" id="freq-sweep" href="#freq-sweep-modal">
진동수 증가
</a>
</div>
</section>
<section class="section amplitude">
<h3>진폭</h3>
<div class="range-box">
<p class="range-field">
<input type="range" id="amp" min="-100" max="0" value="0"/>
</p>
<div class="input-field inline">
<input id="amp-text" type="number" value="0">
<label for="amp-text">진폭 (dBFS)</label>
</div>
<a class="btn waves-effect waves-light modal-trigger" id="amp-sweep" href="#amp-sweep-modal">
진폭 증가
</a>
</div>
</section>
<section class="section play">
<button id="play" class="btn waves-effect waves-light" type="button">재생</button>
<button id="osc-pause" class="btn waves-effect waves-light" type="button">오실로스코프 일시정지</button>
</section>
<section class="section play">
<button id="sample-play" class="btn waves-effect waves-light" type="button">샘플 음원 재생</button>
</section>
</form>
</section>
<aside>
<canvas></canvas>
<p>이 오실로스코프는 샘플링 개수에 한계가 있기 때문에 (특히 진동수가 높아질 수록) 부정확합니다.</p>
</aside>
</main>
<section id="freq-sweep-modal" class="modal">
<div class="modal-content">
<h3>진동수 증가 (Sweep)</h3>
<div class="input-field">
<input id="freq-sweep-start" type="number" value="20">
<label for="freq-sweep-start">시작</label>
</div>
<div class="input-field">
<input id="freq-sweep-speed" type="number" value="1000">
<label for="freq-sweep-speed">Hz/s</label>
</div>
<div class="input-field">
<input id="freq-sweep-end" type="number" value="20000">
<label for="freq-sweep-end">끝</label>
</div>
</div>
<div class="modal-footer">
<a href="#!" id="freq-start" class="modal-action modal-close waves-effect waves-teal btn-flat">시작</a>
<a href="#!" class="modal-action modal-close waves-effect waves-orange btn-flat">취소</a>
</div>
</section>
<section id="amp-sweep-modal" class="modal">
<div class="modal-content">
<h3>진폭 증가 (Sweep)</h3>
<div class="input-field">
<input id="amp-sweep-start" type="number" value="0">
<label for="amp-sweep-start">시작</label>
</div>
<div class="input-field">
<input id="amp-sweep-speed" type="number" value="-1">
<label for="amp-sweep-speed">dBFS/s</label>
</div>
<div class="input-field">
<input id="amp-sweep-end" type="number" value="-20">
<label for="amp-sweep-end">끝</label>
</div>
</div>
<div class="modal-footer">
<a href="#!" id="amp-start" class="modal-action modal-close waves-effect waves-teal btn-flat">시작</a>
<a href="#!" class="modal-action modal-close waves-effect waves-orange btn-flat">취소</a>
</div>
</section>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/js/materialize.min.js"></script>
<script src="https://unpkg.com/babel-transform-in-browser@6.4.6/dist/btib.min.js"></script>
<script type="text/es2015">
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
if(!AudioContext) alert('웹 오디오 API를 지원하지 않는 브라우저 입니다! 최신버전의 크롬을 이용해주세요.');
const context = new AudioContext;
const sampleAudio = new Audio;
const sample = context.createMediaElementSource(sampleAudio);
const oscillator = context.createOscillator();
const gain = context.createGain();
const analyser = context.createAnalyser();
sampleAudio.src = 'https://cdn.rawgit.com/HelloWorld017/restaurance/master/resources/storm-best.mp3';
sampleAudio.crossOrigin = "anonymous";
oscillator.frequency.value = 2000;
oscillator.type = 'sine';
oscillator.start();
gain.gain.value = 1;
analyser.fftSize = 2048;
sample.connect(gain);
gain.connect(analyser);
analyser.connect(context.destination);
let count = 0;
let resizeReserved = false;
const drawWave = (ctx) => {
const len = analyser.frequencyBinCount;
const buffer = new Float32Array(len);
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const ratioX = 3;
const ratioY = 100;
ctx.strokeStyle = "#009688";
ctx.setTransform(1, 0, 0, -1, 0, h / 2);
ctx.lineWidth = 2;
resizeReserved = false;
const loop = () => {
if(oscPlaying) {
analyser.getFloatTimeDomainData(buffer);
ctx.clearRect(0, -h/2, w, h);
ctx.beginPath();
ctx.moveTo(0, buffer[0] * ratioY);
for (let x = 1; x < len; x++) {
const calculatedX = x * ratioX;
if(calculatedX > w) continue;
ctx.lineTo(calculatedX, buffer[x] * ratioY);
}
ctx.stroke();
}
if(!resizeReserved) requestAnimationFrame(loop);
else drawWave(ctx);
};
loop();
};
let isPlaying = false;
let oscPlaying = true;
const togglePlay = () => {
if(isPlaying) {
oscillator.disconnect(gain);
$('#play').innerText = '재생';
} else {
oscillator.connect(gain);
$('#play').innerText = '정지';
}
isPlaying = !isPlaying;
};
const toggleOsc = () => {
if(oscPlaying) {
$('#osc-pause').innerText = '오실로스코프 다시시작';
} else {
$('#osc-pause').innerText = '오실로스코프 일시정지';
}
oscPlaying = !oscPlaying;
};
const updateShape = () => {
const shape = $('input[name="shape"]:checked').value;
const customText = $$('.fourier-text input');
if(shape === 'custom') {
customText.forEach((v) => v.disabled = null);
const imagRaw = $('#custom-text-imag').value.split(',');
imagRaw.map(i => parseFloat(i.trim()));
const realRaw = $('#custom-text-real').value.split(',');
realRaw.map(i => parseFloat(i.trim()));
if(imagRaw.concat(realRaw).some(v => isNaN(v)) || imagRaw.length !== realRaw.length) {
customText.forEach(v => {
v.classList.remove('valid');
v.classList.add('invalid');
});
} else {
oscillator.setPeriodicWave(context.createPeriodicWave(realRaw, imagRaw));
customText.forEach(v => {
v.classList.remove('invalid');
v.classList.add('valid');
});
}
} else {
customText.forEach((v) => v.disabled = 'disabled');
oscillator.type = shape;
}
};
const updateAmp = (val) => {
gain.gain.value = Math.pow(10, val / 20);
};
const updateFreq = (val) => oscillator.frequency.value = val;
const bindTextRange = ({name, callback}) => {
const range = $(`#${name}`);
const text = $(`#${name}-text`);
const cb = (...args) => {
Materialize.updateTextFields();
callback(...args);
};
range.addEventListener('input', () => {
text.value = range.value;
cb(range.value);
});
text.addEventListener('input', () => {
range.value = text.value;
cb(text.value);
});
return {name, callback};
};
const sweep = ({name, callback}) => {
const range = $(`#${name}`);
const text = $(`#${name}-text`);
const button = $(`#${name}-start`);
let current, speed, end, startDiff, sweeping;
const reset = () => {
current = parseFloat($(`#${name}-sweep-start`).value);
speed = parseFloat($(`#${name}-sweep-speed`).value) / 20;
end = parseFloat($(`#${name}-sweep-end`).value);
startDiff = Math.sign(end - current);
sweeping = false;
button.innerText = "시작";
};
const start = () => {
setTimeout(() => {
sweeping = true;
button.innerText = "정지";
sweepStep();
}, 75);
};
const cb = (...args) => {
Materialize.updateTextFields();
callback(...args);
};
const sweepStep = () => {
if(!sweeping) return;
current += speed;
range.value = text.value = current;
cb(current);
if(sweeping && startDiff === Math.sign(end - current)) setTimeout(sweepStep, 50);
};
button.addEventListener('click', () => {
if(sweeping) return reset();
reset();
start();
});
};
const canvas = $('canvas');
const calcCanvas = () => {
const rect = canvas.getBoundingClientRect();
[canvas.width, canvas.height] = [rect.width, rect.height];
resizeReserved = true;
};
$('#play').addEventListener('click', togglePlay);
$('#osc-pause').addEventListener('click', toggleOsc);
$('#sample-play').addEventListener('click', () => {
if(count === 1) sampleAudio.src = 'https://cdn.rawgit.com/HelloWorld017/restaurance/master/resources/storm-love.mp3';
sampleAudio.play();
count++;
});
window.addEventListener('resize', () => {
calcCanvas();
});
calcCanvas();
drawWave(canvas.getContext('2d'));
[...$$('.fourier-text input')]
.concat([...$$('input[name="shape"]')])
.forEach((e) => e.addEventListener('change', updateShape));
$$('.fourier-text input[type="text"]').forEach(v => v.addEventListener('blur', updateShape));
sweep(bindTextRange({
name: 'amp',
callback: updateAmp
}));
sweep(bindTextRange({
name: 'freq',
callback: updateFreq
}));
jQuery('.modal').modal();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment