{"id":485502,"date":"2026-06-29T13:59:52","date_gmt":"2026-06-29T13:59:52","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=485502"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=485502","title":{"rendered":"FM-Synthesis in the Browser. Part 1"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Let\u2019s explore the possibilities of sound synthesis in browsers. We\u2019ll explore the basics and, as a practical example, create a Yamaha DX7 synthesizer emulator.<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/a7\/68\/17\/a7681744d9a860fa26a004a3faf5d546.jpg\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/a7\/68\/17\/a7681744d9a860fa26a004a3faf5d546.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/a7\/68\/17\/a7681744d9a860fa26a004a3faf5d546.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<hr\/>\n<h3>Web Audio API<\/h3>\n<p>Browsers allow you to call JavaScript objects to control and create sound. Documentation: <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Audio_API\" rel=\"noopener noreferrer nofollow\">https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Audio_API<\/a><\/p>\n<p>The API provides components for creating and modifying audio signals. These components can be connected together, and their properties can be changed on a schedule.<\/p>\n<h3>Hello World!<\/h3>\n<p>Let\u2019s look at a simple example, something like the standard \u201cHello World!\u201d for programming languages.<\/p>\n<details class=\"spoiler\">\n<summary>Example HTML page code with explanatory comments.<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;beep&lt;\/button&gt; &lt;!-- create a button--&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();\/\/create the main objectlet when = audioContext.currentTime + 0.1;\/\/start timelet beep = audioContext.createOscillator();\/\/beep objectbeep.frequency.value = 440;\/\/frequency of note Abeep.connect(audioContext.destination);\/\/send sound to the outputbeep.start(when);\/\/start soundbeep.stop(when + 1);\/\/stop sound after 1 second}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>Open the page <a href=\"https:\/\/mzxbox.ru\/fmsynth\/beep.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/beep.html<\/a> in your browser and listen to the sound signal.<\/p>\n<h3>Sound envelope<\/h3>\n<p>If you pluck a guitar string or press a piano key, you\u2019ll notice the difference between the natural sound and the computer-generated sound. The key\u2019s sound increases in volume at the moment you press it and gradually fades over time. In synthesizers, this effect is achieved by adjusting the ADSR envelope:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/0e\/47\/e1\/0e47e1e78e8cdbaef2e1a620b79654d0.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/0e\/47\/e1\/0e47e1e78e8cdbaef2e1a620b79654d0.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/0e\/47\/e1\/0e47e1e78e8cdbaef2e1a620b79654d0.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>More &#8212; <a href=\"https:\/\/en.wikipedia.org\/wiki\/ADSR\" rel=\"noopener noreferrer nofollow\">https:\/\/en.wikipedia.org\/wiki\/ADSR<\/a><\/p>\n<p>Let\u2019s expand on our previous example and add an envelope. Here\u2019s the component connection diagram:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/98\/c3\/43\/98c343278cac4a2a3081922ee6581e5b.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/98\/c3\/43\/98c343278cac4a2a3081922ee6581e5b.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/98\/c3\/43\/98c343278cac4a2a3081922ee6581e5b.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<details class=\"spoiler\">\n<summary>Example code with comments<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;beep AHDSR&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();\/\/create the main objectlet when = audioContext.currentTime + 0.1;\/\/start timelet beep = audioContext.createOscillator();\/\/beep objectlet envelope = audioContext.createGain();\/\/gain node to set envelope of soundbeep.frequency.value = 440;\/\/\u0447\u0430\u0441\u0442\u043e\u0442\u0430 \u043d\u043e\u0442\u044b \u041b\u044fenvelope.gain.setValueAtTime(0, when);\/\/0 at beginingenvelope.gain.linearRampToValueAtTime(1, when + 0.05);\/\/gradually increase to 1 in 0.5 secenvelope.gain.linearRampToValueAtTime(0.5, when + 0.2);\/\/reduce to 0.5 in 0.2 senvelope.gain.setValueAtTime(0.5, when + 0.99);\/\/setup last volume valueenvelope.gain.linearRampToValueAtTime(0, when + 1);\/\/reduce to 0 at endenvelope.connect(audioContext.destination);\/\/send final sound to outputbeep.connect(envelope);\/\/send beep sound to envelope cahngerbeep.start(when);beep.stop(when + 1);}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>Open the page <a href=\"https:\/\/mzxbox.ru\/fmsynth\/envelope.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/envelope.html<\/a> in your browser &#8212; the sound will be smooth, louder at the beginning and quieter at the end.<\/p>\n<h3>Modulation of sound signal<\/h3>\n<p>Audio modulation is the process of modifying a carrier signal using a modulator signal. More information can be found at: <a href=\"https:\/\/en.wikipedia.org\/wiki\/Modulation\" rel=\"noopener noreferrer nofollow\">https:\/\/en.wikipedia.org\/wiki\/Modulation<\/a><\/p>\n<p>Amplitude modulation is a type of modulation in which the variable parameter of the carrier signal is its amplitude.<\/p>\n<p>Frequency modulation is a type of analog modulation in which the modulating signal controls the frequency of the carrier wave. Compared to amplitude modulation, the amplitude remains constant.<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/1q\/jx\/jh\/1qjxjhglliwwncxzmxfvkujz_hw.gif\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/webt\/1q\/jx\/jh\/1qjxjhglliwwncxzmxfvkujz_hw.gif 780w,&#10;       https:\/\/habrastorage.org\/webt\/1q\/jx\/jh\/1qjxjhglliwwncxzmxfvkujz_hw.gif 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>Amplitude modulation<\/h4>\n<p>Sound from a modulator connected to the gain parameter of a node whose input is connected to a carrier:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/f7\/f0\/fe\/f7f0fe13b8b812aedcaf60f4b1d250ca.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/f7\/f0\/fe\/f7f0fe13b8b812aedcaf60f4b1d250ca.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/f7\/f0\/fe\/f7f0fe13b8b812aedcaf60f4b1d250ca.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<details class=\"spoiler\">\n<summary>Code example<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;Amplitude modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let result = audioContext.createGain();let level = audioContext.createGain();carrier.frequency.value = 500;modulator.frequency.value = 4;level.gain.value = 0.5;\/\/reduce to 0.5result.gain.value = 0.5;\/\/shift to 0.5modulator.connect(level);level.connect(result.gain);carrier.connect(result);result.connect(audioContext.destination);carrier.start(when);modulator.start(when);carrier.stop(when + 2);modulator.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>The generator produces a sine wave signal [-1; +1], but the volume should change as [0; +1] &#8212; therefore, additional transformations are required.<\/p>\n<p>Open the page to listen to the sound <a href=\"https:\/\/mzxbox.ru\/fmsynth\/amplitude.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/amplitude.html<\/a><\/p>\n<h4>Frequency modulation<\/h4>\n<p>Connection mix:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/0a\/ff\/3c\/0aff3c30837dca3c5fe08dc8a8e2a239.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/0a\/ff\/3c\/0aff3c30837dca3c5fe08dc8a8e2a239.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/0a\/ff\/3c\/0aff3c30837dca3c5fe08dc8a8e2a239.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<details class=\"spoiler\">\n<summary>Code example<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;frequency modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let level = audioContext.createGain();modulator.frequency.value = 4;level.gain.value = 200;carrier.frequency.value = 300;carrier.connect(audioContext.destination);level.connect(carrier.frequency);modulator.connect(level);carrier.start(when);modulator.start(when);carrier.stop(when + 2);modulator.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>Open the page to listen to the sound <a href=\"https:\/\/mzxbox.ru\/fmsynth\/frequency.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/frequency.html<\/a><\/p>\n<h3>Phase modulation<\/h3>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/85\/6b\/40\/856b40eef6db0faf98805b6244beedf9.gif\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/webt\/85\/6b\/40\/856b40eef6db0faf98805b6244beedf9.gif 780w,&#10;       https:\/\/habrastorage.org\/webt\/85\/6b\/40\/856b40eef6db0faf98805b6244beedf9.gif 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>Phase modulation shifts the phase of a signal, adding overtones and ultimately changing the timbre. For more information, see <a href=\"https:\/\/en.wikipedia.org\/wiki\/Phase_modulation\" rel=\"noopener noreferrer nofollow\">https:\/\/en.wikipedia.org\/wiki\/Phase_modulation<\/a><\/p>\n<p>Connection mix:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/03\/92\/6e\/03926ee8a938c2b202342a08b208b1df.png\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/webt\/03\/92\/6e\/03926ee8a938c2b202342a08b208b1df.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/webt\/03\/92\/6e\/03926ee8a938c2b202342a08b208b1df.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<details class=\"spoiler\">\n<summary>\u041a\u043e\u0434 \u043f\u0440\u0438\u043c\u0435\u0440\u0430<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;phase modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let soundFrequency = 500;\/\/frequencylet maxmodulation = 4;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let level = audioContext.createGain();let phaseDelay = audioContext.createDelay();carrier.frequency.value = soundFrequency;modulator.frequency.value = soundFrequency;level.gain.setValueAtTime(0, when);level.gain.linearRampToValueAtTime(maxmodulation \/ (2 * Math.PI * soundFrequency), when + 2);phaseDelay.delayTime.value = 0.5 \/ soundFrequency;\/\/shift by wave sizemodulator.connect(level);level.connect(phaseDelay.delayTime);carrier.connect(phaseDelay);phaseDelay.connect(audioContext.destination);modulator.start(when);carrier.start(when);modulator.stop(when + 2);carrier.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>Open the page to listen to the sound <a href=\"https:\/\/mzxbox.ru\/fmsynth\/phase.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/phase.html<\/a><\/p>\n<h4>AudioWorklet<\/h4>\n<p>The Web Audio API provides ready-made components for working with audio. Additionally, there\u2019s an AudioWorkletProcessor component that allows you to write your own digital signal processing (DSP) code.<\/p>\n<p>More &#8212; <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/AudioWorkletProcessor\" rel=\"noopener noreferrer nofollow\">https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/AudioWorkletProcessor<\/a><\/p>\n<details class=\"spoiler\">\n<summary>Here is the phase modulation code from the previous example, but using AudioWorkletProcessor.<\/summary>\n<div class=\"spoiler__content\">\n<pre><code>&lt;html&gt;&lt;button onclick='start();'&gt;phase worklet&lt;\/button&gt;&lt;script&gt;let phaseWorkletSource = `class PhaseSineAudioWorkletProcessor extends AudioWorkletProcessor {phase = 0;cntr = 0;constructor() {super();}static get parameterDescriptors() {return [{ name: \"carrierFrequency\", automationRate: \"a-rate\" }, { name: \"modulationLevel\", automationRate: \"a-rate\" }];}readSample(inputs, xx) {let inputSumm = 0;for (let ii = 0; ii &lt; inputs.length; ii++) {let singleInput = inputs[ii];let channelCount = singleInput.length;if (channelCount) {let channelSumm = 0;for (let ch = 0; ch &lt; singleInput.length; ch++) {let singleChannel = singleInput[ch];channelSumm = channelSumm + singleChannel[xx];}inputSumm = inputSumm + channelSumm \/ channelCount;}}return inputSumm;}writeSample(outputs, xx, value) {for (let oo = 0; oo &lt; outputs.length; oo++) {let singleOutput = outputs[oo];for (let ch = 0; ch &lt; singleOutput.length; ch++) {let singleChannel = singleOutput[ch];singleChannel[xx] = value;}}}process(inputs, outputs, parameters) {let outSampleCount = outputs[0][0].length;let frequency = parameters[\"carrierFrequency\"][0];let modulationLevel = parameters[\"modulationLevel\"][0];let incrementBySample = Math.PI * 2 * frequency \/ sampleRate;for (let xx = 0; xx &lt; outSampleCount; xx++) {let inputSumm = this.readSample(inputs, xx);let resultValue = Math.sin(this.phase + modulationLevel * inputSumm);this.writeSample(outputs, xx, resultValue);this.phase = this.phase + incrementBySample;if (this.phase &gt;= Math.PI * 2) {this.phase = this.phase - Math.PI * 2;}}return true;}}registerProcessor(\"sinePhaseModuleID\", PhaseSineAudioWorkletProcessor);`;function loadAudioWorkletCode(audioworkletcode, audioContext, onDone) {let blob = new Blob([audioworkletcode], { type: 'application\/javascript' });let reader = new FileReader();reader.onloadend = function () {let blobURL = reader.result;audioContext.audioWorklet.addModule(blobURL).then((vv) =&gt; {onDone();});}reader.readAsDataURL(blob);}function start() {let audioContext = new AudioContext();loadAudioWorkletCode(phaseWorkletSource, audioContext, () =&gt; {playSound(audioContext);});}function playSound(audioContext) {let when = audioContext.currentTime + 0.1;let soundFrequency = 500;let maxmodulation = 4;let carrier = new AudioWorkletNode(audioContext, 'sinePhaseModuleID');let modulatorBeep = audioContext.createOscillator();let volume = audioContext.createGain();volume.gain.value = 0;let descriptors = carrier.parameters;let carrierFrequency = descriptors.get('carrierFrequency');let modulationLevel = descriptors.get('modulationLevel');carrierFrequency.value = soundFrequency;modulatorBeep.frequency.value = soundFrequency;modulationLevel.setValueAtTime(0, when);modulationLevel.linearRampToValueAtTime(maxmodulation, when + 2);volume.connect(audioContext.destination);carrier.connect(volume);modulatorBeep.connect(carrier);modulatorBeep.start(when);volume.gain.setValueAtTime(1, when);volume.gain.setValueAtTime(0, when + 2);}&lt;\/script&gt;&lt;\/html&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<\/details>\n<p>Open the page to listen to the sound <a href=\"https:\/\/mzxbox.ru\/fmsynth\/phaseworklet.html\" rel=\"noopener noreferrer nofollow\">https:\/\/mzxbox.ru\/fmsynth\/phaseworklet.html<\/a><\/p>\n<p>As you can see, the result sounds the same as in the previous example, but the code has become much larger.<\/p>\n<p>This approach only makes sense if you need to recompile your C++ VST plugin code to WebAssembly (<a href=\"https:\/\/wikipedia.org\/wiki\/WebAssembly\" rel=\"noopener noreferrer nofollow\">https:\/\/wikipedia.org\/wiki\/WebAssembly<\/a>) to run it in a browser.<\/p>\n<h3>Practice of use<\/h3>\n<p>After reading this, you may be wondering, \u201cWhy use FM synthesis in a browser?\u201d<\/p>\n<p>Answer: \u201cOf course, to create music synthesizers!\u201d<\/p>\n<p>Desktop DAWs (Ableton Live, FL Studio, etc.) support plugin APIs (see <a href=\"https:\/\/wikipedia.org\/wiki\/Virtual_Studio_Technology\" rel=\"noopener noreferrer nofollow\">https:\/\/wikipedia.org\/wiki\/Virtual_Studio_Technology<\/a>). Anyone can write their own electronic instrument that will work in any DAW or sequencer.<\/p>\n<p>In modern music, the importance of plugins is so great that beginners ask not \u201cWhich editor should I use for XXX music?\u201d, but \u201cWhich plugin should I buy for XXX music?\u201d<\/p>\n<p>The situation is much worse online. Even <a href=\"https:\/\/blog.bandlab.com\/30-million-of-you-on-bandlab\/\" rel=\"noopener noreferrer nofollow\">Bandlab<\/a> (30 million users) lacks an API for expanding its studio\u2019s capabilities with third-party plugins.<\/p>\n<p>I\u2019m currently working on an online sequencer that implements its own plugin API. It\u2019s still a long way off from release, but you can try it out now.<\/p>\n<h4>Emulation of Yamaha DX7<\/h4>\n<p>The Yamaha DX7 is a digital synthesizer released by Yamaha in 1983. It was very popular in the 1980s and, largely due to its low cost and compact size, became one of the best-selling models in synthesizer history &#8212; <a href=\"https:\/\/wikipedia.org\/wiki\/Yamaha_DX7\" rel=\"noopener noreferrer nofollow\">https:\/\/wikipedia.org\/wiki\/Yamaha_DX7<\/a><\/p>\n<p>The Yamaha DX7 synthesizer uses phase modulation to synthesize instruments. Over the years, thousands of presets have been created for it, ranging from guitars to traditional and futuristic instruments.<\/p>\n<p>You can see the plugin in action here:<\/p>\n<p><a href=\"https:\/\/rutube.ru\/video\/69cec43733da30418e9d56bd5225303c\/\" rel=\"noopener noreferrer nofollow\">https:\/\/rutube.ru\/video\/69cec43733da30418e9d56bd5225303c\/<\/a><\/p>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/6a427a58a269d5af840140f0\" data-style=\"\" id=\"6a427a58a269d5af840140f0\" width=\"\" data-habr-games=\"\"><\/div>\n<p>Due of using of the Web Audio API, the plugin code is much more compact in compare to existing VST implementations.<\/p>\n<p>The text is too long, so code analysis and sound synthesis in DX7 will be discussed in the second part of the article.<\/p>\n<p>Original &#8212; <a href=\"https:\/\/habr.com\/ru\/articles\/1052640\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/articles\/1052640\/<\/a><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1053484\/\">https:\/\/habr.com\/ru\/articles\/1053484\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let\u2019s explore the possibilities of sound synthesis in browsers. We\u2019ll explore the basics and, as a practical example, create a Yamaha DX7 synthesizer emulator.Web Audio APIBrowsers allow you to call JavaScript objects to control and create sound. Documentation: https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Audio_APIThe API provides components for creating and modifying audio signals. These components can be connected together, and their properties can be changed on a schedule.Hello World!Let\u2019s look at a simple example, something like the standard \u201cHello World!\u201d for programming languages.Example HTML page code with explanatory comments.&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;beep&lt;\/button&gt; &lt;!&#8212; create a button&#8212;&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();\/\/create the main objectlet when = audioContext.currentTime + 0.1;\/\/start timelet beep = audioContext.createOscillator();\/\/beep objectbeep.frequency.value = 440;\/\/frequency of note Abeep.connect(audioContext.destination);\/\/send sound to the outputbeep.start(when);\/\/start soundbeep.stop(when + 1);\/\/stop sound after 1 second}&lt;\/script&gt;&lt;\/html&gt;Open the page https:\/\/mzxbox.ru\/fmsynth\/beep.html in your browser and listen to the sound signal.Sound envelopeIf you pluck a guitar string or press a piano key, you\u2019ll notice the difference between the natural sound and the computer-generated sound. The key\u2019s sound increases in volume at the moment you press it and gradually fades over time. In synthesizers, this effect is achieved by adjusting the ADSR envelope:More &#8212; https:\/\/en.wikipedia.org\/wiki\/ADSRLet\u2019s expand on our previous example and add an envelope. Here\u2019s the component connection diagram:Example code with comments&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;beep AHDSR&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();\/\/create the main objectlet when = audioContext.currentTime + 0.1;\/\/start timelet beep = audioContext.createOscillator();\/\/beep objectlet envelope = audioContext.createGain();\/\/gain node to set envelope of soundbeep.frequency.value = 440;\/\/\u0447\u0430\u0441\u0442\u043e\u0442\u0430 \u043d\u043e\u0442\u044b \u041b\u044fenvelope.gain.setValueAtTime(0, when);\/\/0 at beginingenvelope.gain.linearRampToValueAtTime(1, when + 0.05);\/\/gradually increase to 1 in 0.5 secenvelope.gain.linearRampToValueAtTime(0.5, when + 0.2);\/\/reduce to 0.5 in 0.2 senvelope.gain.setValueAtTime(0.5, when + 0.99);\/\/setup last volume valueenvelope.gain.linearRampToValueAtTime(0, when + 1);\/\/reduce to 0 at endenvelope.connect(audioContext.destination);\/\/send final sound to outputbeep.connect(envelope);\/\/send beep sound to envelope cahngerbeep.start(when);beep.stop(when + 1);}&lt;\/script&gt;&lt;\/html&gt;Open the page https:\/\/mzxbox.ru\/fmsynth\/envelope.html in your browser &#8212; the sound will be smooth, louder at the beginning and quieter at the end.Modulation of sound signalAudio modulation is the process of modifying a carrier signal using a modulator signal. More information can be found at: https:\/\/en.wikipedia.org\/wiki\/ModulationAmplitude modulation is a type of modulation in which the variable parameter of the carrier signal is its amplitude.Frequency modulation is a type of analog modulation in which the modulating signal controls the frequency of the carrier wave. Compared to amplitude modulation, the amplitude remains constant.Amplitude modulationSound from a modulator connected to the gain parameter of a node whose input is connected to a carrier:Code example&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;Amplitude modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let result = audioContext.createGain();let level = audioContext.createGain();carrier.frequency.value = 500;modulator.frequency.value = 4;level.gain.value = 0.5;\/\/reduce to 0.5result.gain.value = 0.5;\/\/shift to 0.5modulator.connect(level);level.connect(result.gain);carrier.connect(result);result.connect(audioContext.destination);carrier.start(when);modulator.start(when);carrier.stop(when + 2);modulator.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;The generator produces a sine wave signal [-1; +1], but the volume should change as [0; +1] &#8212; therefore, additional transformations are required.Open the page to listen to the sound https:\/\/mzxbox.ru\/fmsynth\/amplitude.htmlFrequency modulationConnection mix:Code example&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;frequency modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let level = audioContext.createGain();modulator.frequency.value = 4;level.gain.value = 200;carrier.frequency.value = 300;carrier.connect(audioContext.destination);level.connect(carrier.frequency);modulator.connect(level);carrier.start(when);modulator.start(when);carrier.stop(when + 2);modulator.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;Open the page to listen to the sound https:\/\/mzxbox.ru\/fmsynth\/frequency.htmlPhase modulationPhase modulation shifts the phase of a signal, adding overtones and ultimately changing the timbre. For more information, see https:\/\/en.wikipedia.org\/wiki\/Phase_modulationConnection mix:\u041a\u043e\u0434 \u043f\u0440\u0438\u043c\u0435\u0440\u0430&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;phase modulation&lt;\/button&gt;&lt;script&gt;function start() {let audioContext = new AudioContext();let when = audioContext.currentTime + 0.1;let soundFrequency = 500;\/\/frequencylet maxmodulation = 4;let carrier = audioContext.createOscillator();let modulator = audioContext.createOscillator();let level = audioContext.createGain();let phaseDelay = audioContext.createDelay();carrier.frequency.value = soundFrequency;modulator.frequency.value = soundFrequency;level.gain.setValueAtTime(0, when);level.gain.linearRampToValueAtTime(maxmodulation \/ (2 * Math.PI * soundFrequency), when + 2);phaseDelay.delayTime.value = 0.5 \/ soundFrequency;\/\/shift by wave sizemodulator.connect(level);level.connect(phaseDelay.delayTime);carrier.connect(phaseDelay);phaseDelay.connect(audioContext.destination);modulator.start(when);carrier.start(when);modulator.stop(when + 2);carrier.stop(when + 2);}&lt;\/script&gt;&lt;\/html&gt;Open the page to listen to the sound https:\/\/mzxbox.ru\/fmsynth\/phase.htmlAudioWorkletThe Web Audio API provides ready-made components for working with audio. Additionally, there\u2019s an AudioWorkletProcessor component that allows you to write your own digital signal processing (DSP) code.More &#8212; https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/AudioWorkletProcessorHere is the phase modulation code from the previous example, but using AudioWorkletProcessor.&lt;html&gt;&lt;button onclick=&#8217;start();&#8217;&gt;phase worklet&lt;\/button&gt;&lt;script&gt;let phaseWorkletSource = `class PhaseSineAudioWorkletProcessor extends AudioWorkletProcessor {phase = 0;cntr = 0;constructor() {super();}static get parameterDescriptors() {return [{ name: &#171;carrierFrequency&#187;, automationRate: &#171;a-rate&#187; }, { name: &#171;modulationLevel&#187;, automationRate: &#171;a-rate&#187; }];}readSample(inputs, xx) {let inputSumm = 0;for (let ii = 0; ii &lt; inputs.length; ii++) {let singleInput = inputs[ii];let channelCount = singleInput.length;if (channelCount) {let channelSumm = 0;for (let ch = 0; ch &lt; singleInput.length; ch++) {let singleChannel = singleInput[ch];channelSumm = channelSumm + singleChannel[xx];}inputSumm = inputSumm + channelSumm \/ channelCount;}}return inputSumm;}writeSample(outputs, xx, value) {for (let oo = 0; oo &lt; outputs.length; oo++) {let singleOutput = outputs[oo];for (let ch = 0; ch &lt; singleOutput.length; ch++) {let singleChannel = singleOutput[ch];singleChannel[xx] = value;}}}process(inputs, outputs, parameters) {let outSampleCount = outputs[0][0].length;let frequency = parameters[&#171;carrierFrequency&#187;][0];let modulationLevel = parameters[&#171;modulationLevel&#187;][0];let incrementBySample = Math.PI * 2 * frequency \/ sampleRate;for (let xx = 0; xx &lt; outSampleCount; xx++) {let inputSumm = this.readSample(inputs, xx);let resultValue = Math.sin(this.phase + modulationLevel * inputSumm);this.writeSample(outputs, xx, resultValue);this.phase = this.phase + incrementBySample;if (this.phase &gt;= Math.PI * 2) {this.phase = this.phase &#8212; Math.PI * 2;}}return true;}}registerProcessor(&#171;sinePhaseModuleID&#187;, PhaseSineAudioWorkletProcessor);`;function loadAudioWorkletCode(audioworkletcode, audioContext, onDone) {let blob = new Blob([audioworkletcode], { type: &#8216;application\/javascript&#8217; });let reader = new FileReader();reader.onloadend = function () {let blobURL = reader.result;audioContext.audioWorklet.addModule(blobURL).then((vv) =&gt; {onDone();});}reader.readAsDataURL(blob);}function start() {let audioContext = new AudioContext();loadAudioWorkletCode(phaseWorkletSource, audioContext, () =&gt; {playSound(audioContext);});}function playSound(audioContext) {let when = audioContext.currentTime + 0.1;let soundFrequency = 500;let maxmodulation = 4;let carrier = new AudioWorkletNode(audioContext, &#8216;sinePhaseModuleID&#8217;);let modulatorBeep = audioContext.createOscillator();let volume = audioContext.createGain();volume.gain.value = 0;let descriptors = carrier.parameters;let carrierFrequency = descriptors.get(&#8216;carrierFrequency&#8217;);let modulationLevel = descriptors.get(&#8216;modulationLevel&#8217;);carrierFrequency.value = soundFrequency;modulatorBeep.frequency.value = soundFrequency;modulationLevel.setValueAtTime(0, when);modulationLevel.linearRampToValueAtTime(maxmodulation, when + 2);volume.connect(audioContext.destination);carrier.connect(volume);modulatorBeep.connect(carrier);modulatorBeep.start(when);volume.gain.setValueAtTime(1, when);volume.gain.setValueAtTime(0, when + 2);}&lt;\/script&gt;&lt;\/html&gt;Open the page to listen to the sound https:\/\/mzxbox.ru\/fmsynth\/phaseworklet.htmlAs you can see, the result sounds the same as in the previous&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-485502","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/485502","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=485502"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/485502\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=485502"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=485502"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=485502"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}