diff --git a/piano/synth/AudioPlayer.mjs b/piano/synth/AudioPlayer.mjs
new file mode 100644
index 0000000..2867b0c
--- /dev/null
+++ b/piano/synth/AudioPlayer.mjs
@@ -0,0 +1,59 @@
+import Timbre from "./Timbre.mjs";
+
+export default class AudioPlayer {
+ /** @type {Timbre} */
+ timbre = null;
+ frequency = null;
+ oscillatorNode = null;
+ gainNode = null;
+
+ constructor(timbre, frequency) {
+ this.timbre = timbre;
+ this.frequency = frequency;
+
+ this.oscillatorNode = timbre.audioCtx.createOscillator();
+ if (this.timbre.type === 'custom') {
+ this.oscillatorNode.setPeriodicWave(timbre.wave);
+ } else {
+ this.oscillatorNode.type = timbre.type;
+ }
+ this.oscillatorNode.frequency.value = frequency;
+
+ this.gainNode = timbre.audioCtx.createGain();
+ this.gainNode.gain.value = 0;
+
+ this.oscillatorNode
+ .connect(this.gainNode)
+ .connect(timbre.audioCtx.destination);
+ this.oscillatorNode.start();
+ }
+
+ start() {
+ const adsr = this.timbre.adsr;
+ let time = this.timbre.audioCtx.currentTime;
+ this.gainNode.gain.cancelScheduledValues(time);
+
+ this.gainNode.gain.setValueAtTime(0, time);
+
+ time += adsr.attackDuration;
+ this.gainNode.gain.linearRampToValueAtTime(adsr.attackAmplitude, time);
+
+ time += adsr.decayDuration;
+ this.gainNode.gain.linearRampToValueAtTime(adsr.decayAmplitude, time);
+
+ if (adsr.sustainDuration) {
+ time += adsr.sustainDuration;
+ this.gainNode.gain.linearRampToValueAtTime(0, time);
+ }
+ }
+
+ stop() {
+ const adsr = this.timbre.adsr;
+ let time = this.timbre.audioCtx.currentTime;
+ this.gainNode.gain.cancelScheduledValues(time);
+
+ time += adsr.releaseDuration;
+ this.gainNode.gain.linearRampToValueAtTime(0, time);
+ }
+
+};
diff --git a/piano/synth/Timbre.mjs b/piano/synth/Timbre.mjs
new file mode 100644
index 0000000..0aab00f
--- /dev/null
+++ b/piano/synth/Timbre.mjs
@@ -0,0 +1,27 @@
+export default class Timbre {
+ audioCtx = null;
+ type = 'sine';
+ wave = null;
+ adsr = {
+ attackAmplitude: 1,
+ attackDuration: 0.1,
+ decayAmplitude: 0.5,
+ decayDuration: 0.2,
+ sustainDuration: 3,
+ releaseDuration: 1,
+ };
+
+ constructor() {
+ this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ }
+
+ setWithHarmonics(harmonics) {
+ this.harmonics = harmonics;
+
+ this.type = 'custom';
+ this.wave = new PeriodicWave(this.audioCtx, {
+ real: new Float32Array(harmonics),
+ imag: new Float32Array(harmonics.length),
+ });
+ }
+};
diff --git a/piano/synth/index.html b/piano/synth/index.html
index d24e62f..f3e2614 100644
--- a/piano/synth/index.html
+++ b/piano/synth/index.html
@@ -28,6 +28,8 @@