├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── README_RU.md ├── dist └── index.html ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── og.png ├── src ├── BinarySynth.vue ├── assets │ ├── js │ │ ├── fourierCoefficients.js │ │ ├── getFrequency.js │ │ ├── getMIDINote.js │ │ ├── helpers.js │ │ ├── midiMessages.js │ │ └── notes.js │ └── styles │ │ ├── main.scss │ │ ├── normalize.css │ │ └── vars.scss ├── components │ ├── ControlPanel │ │ ├── ControlPanel.vue │ │ ├── Filter.vue │ │ ├── Frequency.vue │ │ ├── Global.vue │ │ ├── InteractiveInput.vue │ │ ├── LFO.vue │ │ ├── Midi.vue │ │ ├── Oscillator.vue │ │ └── SettingsExchange.vue │ ├── FileInput.vue │ └── Status.vue ├── main.js └── stores │ └── globalStore.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist/favicon.ico 13 | dist/og.png 14 | dist-ssr 15 | coverage 16 | *.local 17 | NOTES.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "printWidth": 140, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "requirePragma": false, 11 | "semi": false, 12 | "singleQuote": true, 13 | "tabWidth": 4, 14 | "trailingComma": "es5", 15 | "useTabs": false, 16 | "disableLanguages": [], 17 | "[html]": { 18 | "editor.defaultFormatter": "vscode.html-language-features" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Max Alyokhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binary Synth 2 | 3 | _Audio synthesis from binary code of any file_ 4 | 5 | [![Uptime Robot status](https://img.shields.io/uptimerobot/status/m795264551-bb4c959b31b6ff94b02f9545)](https://bs.stranno.su) [![Uptime Robot status](https://img.shields.io/uptimerobot/ratio/m795264551-bb4c959b31b6ff94b02f9545)](https://bs.stranno.su) 6 | 7 | **Demo**: https://bs.stranno.su 8 | 9 | **Video**: https://youtu.be/5LMYiLwfvRg 10 | 11 | ![](https://store.stranno.su/bs/fuji.png) 12 | 13 | _Эта страница есть также на русском_ 14 | 15 | A web-synthesizer that generates sound from the binary code of any files. It can synthesize sound directly in the browser, or be a generator of MIDI messages to external devices or DAWs, turning any file into a score. All the application code is written in Javascript and along with everything you need is packed into a single .html file of about 750kb. The synthesizer doesn't need internet, it can be downloaded and run locally on any device with a browser. 16 | 17 | The application reads the file sequentially, and due to the high speed of reading and random deviation of reading duration, we can get quite unpredictable generation of timbre nuances, and at certain settings we can switch to granular synthesis. 18 | 19 | You can try some examples of instrument settings that will sound about the same for most files: 20 | 21 | - [IDM](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22sine%22,%22gain%22:2.04,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:0,%22to%22:8},%22notesRange%22:{%22from%22:48,%22to%22:60},%22fragment%22:{%22from%22:0,%22to%22:5000},%22midiMode%22:false,%22biquadFilterFrequency%22:33.1,%22biquadFilterQ%22:112.4,%22LFO%22:{%22enabled%22:true,%22type%22:%22triangle%22,%22rate%22:34.5,%22depth%22:1},%22bitness%22:%2216%22,%22panner%22:-0.67,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:50,%22noMIDIPortsFound%22:true,%22velocity%22:120,%22solidMode%22:false,%22lastNoteOnMode%22:true}}) 22 | - [Ambient](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22triangle%22,%22gain%22:0.41,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:0,%22to%22:1},%22notesRange%22:{%22from%22:48,%22to%22:104},%22fragment%22:{%22from%22:0,%22to%22:624},%22midiMode%22:false,%22biquadFilterFrequency%22:39.3,%22biquadFilterQ%22:121.3,%22LFO%22:{%22enabled%22:true,%22type%22:%22sine%22,%22rate%22:99,%22depth%22:0.395},%22bitness%22:%228%22,%22panner%22:0,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:66,%22noMIDIPortsFound%22:true,%22velocity%22:66,%22solidMode%22:true,%22lastNoteOnMode%22:true}}) 23 | - [Harsh noise](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22sine%22,%22gain%22:2.56,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:41,%22to%22:90},%22notesRange%22:{%22from%22:48,%22to%22:104},%22fragment%22:{%22from%22:0,%22to%22:100720},%22midiMode%22:false,%22biquadFilterFrequency%22:418.5,%22biquadFilterQ%22:66.6,%22LFO%22:{%22enabled%22:true,%22type%22:%22sine%22,%22rate%22:91,%22depth%22:0.546},%22bitness%22:%228%22,%22panner%22:0.15,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:66,%22noMIDIPortsFound%22:true,%22velocity%22:66,%22solidMode%22:true,%22lastNoteOnMode%22:true}}) 24 | 25 | ## Contents 26 | 27 | - [Application principle](#principle) 28 | - [Switching to granular mode](#granular) 29 | - [Recommendations for optimal performance](#recommendation) 30 | - [MIDI](#midi) 31 | - [Interface features](#interface) 32 | - [Saving settings](#settings) 33 | - [Run locally and build the project](#run) 34 | 35 | ## Application principle 36 | 37 | 38 | All data on any computer or smartphone is in the form of files (which are, in essence, texts). The contents of these files are ultimately just zeros and ones. And these zeros and ones are basically all the same, so we need an interpreter to extract meaning from these texts. Basically, the file format (.mp3, .docx, etc.) is just a pointer to which interpreter we need to pass the text in order to extract meaning from it. 39 | 40 | But what if the file format and the interpreter don't match? 41 | 42 | In the case of musical experimentation, there have been earlier attempts, for example, to "play" a file through an audio editor. 43 | 44 | We could go further and write our own interpreter that would look at the files without regard to format, use its own "manner of reading" the original zeros and ones, and on that basis provide a complete system for controlled synthesis of sounds. 45 | 46 | 1. We can interpret files as an array of numbers. That is, we divide continuous machine code into _words_ of some information capacity (bitness): 47 | 48 | - 8 bits (numbers from 0 to 255) 49 | - 16 bits (numbers from 0 to 65 535) 50 | 51 | 2. Then, each word is a command that defines the frequency of the sound 52 | 53 | 3. At the level of the whole system, we set global parameters: 54 | 55 | - speed of interpretation 56 | - musical scale (or lack thereof), range of notes/frequencies 57 | - looping 58 | - MIDI mode 59 | - smooth or abrupt transition between commands 60 | - settings of virtual devices required for synthesis (oscillator, filter, LFO) or MIDI settings 61 | 62 | 4. To reduce the load on the device, we divide the file into chunks of 500 commands each 63 | 64 | 5. Recursively schedule the synthesis control by reading 500 instructions per iteration and using global parameters 65 | 66 | 6. If we have reached the end of the file, stop execution or start again 67 | 68 | ## Switching to granular mode 69 | 70 | 71 | > **Note**: Here and below the instrument interface terms are used. For their description, see below in the Interface features section 72 | 73 | Granular synthesis operates on small pieces of sounds — acoustic pixels. It is generally accepted that granular synthesis "starts" when operating with sounds <50ms. At values `fragment` * `reading speed` = <50 we begin to operate with acoustic pixels. 74 | 75 | In this case, each command from `fragment` can be considered a "subpixel", which, with `random time gap` enabled, is unique each time, and the pixel, respectively, is unique in multiples of the number of subpixels. As a result, we get a mutable timbre. 76 | 77 | In classical granular synthesis, pixels play simultaneously and in parallel, and their number can change over time. In BS, on the other hand, the pixels form a thread along which we move. 78 | 79 | *That is, in conventional granular synthesis, a truck with sand is thrown on the listener, where each grain of sand is an acoustic pixel, but here this sand is poured out through a funnel with the diameter of one grain of sand, and this thin stream is what we observe.* 80 | 81 | The image below shows the formation of an acoustic pixel from two commands (`fragment: from = 0, to = 1`), at a reading speed of 0.005 s. We need to consider that each frequency has a period *T*, equal to the ratio of a unit of time (1 second = 1000 milliseconds) to the frequency. This means that we can think of sound not only in terms of frequency, but also in terms of the time it takes for the wave to make one complete oscillation. If the wave does not have time to make a complete oscillation, such an object is called a "wavelet". 82 | 83 | ![](https://store.stranno.su/bs/granular.jpg) 84 | 85 | ## Recommendations for optimal performance 86 | 87 | 88 | - Use incognito mode with extensions disabled 89 | - Close the non-incognito browser 90 | - Leave only BS tabs in incognito mode 91 | - Use a separate browser [ungoogled-chromium](https://github.com/ungoogled-software/ungoogled-chromium), which uses a little less CPU and a lot less RAM. 92 | 93 | BS under workload requires on average up to 7.1% CPU, in incognito mode 6%, Firefox 4.2%, but runs less stable. Also the browser's open console/DevTools increases CPU consumption per tab by 10%. It is recommended to use BS in incognito mode without any other open tabs except BS tabs for maximum efficiency. 94 | 95 | More interesting sound is obtained with several independent instances of BS in different tabs. Theoretically, it would be possible to implement several BS threads in one tab, but this is less optimal, because browsers limit the maximum CPU usage per tab (in Chrome it is 10% of CPU). Also, each tab has its own [event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop). You can use `ctrl + tab number` to quickly switch between tabs. 96 | 97 | ## MIDI 98 | 99 | 100 | When the MIDI mode is enabled, the first available port and its first channel are automatically selected. Next, a `noteOn` signal is sent sequentially when reading, and a `noteOff` signal is sent after the `reading speed` time. In `continuous` mode, a `Pitch` signal is sent after each noteOn to hit the desired frequency. 101 | 102 | MIDI messages can be sent: 103 | 104 | - to neighboring tabs and browser windows if they are listening to MIDI (e.g., in the web analog [DX7](http://mmontag.github.io/dx7-synth-js)) 105 | - in DAW and other applications with virtual synthesizers (i.e. BS can control, for example, synthesizer in Ableton). 106 | - to MIDI-compatible external devices connected to a computer 107 | 108 | To send MIDI messages to a DAW on Windows devices, you can use [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). 109 | 110 | > **Note**: After any manipulations with MIDI ports (connection/disconnection/re-connection) it is necessary to completely restart the browser, closing all browser windows if there are several of them 111 | 112 | > **Note**: MIDI messages are generated on the desktop only 113 | 114 | ## Interface features 115 | 116 | 117 | - `Reading speed` — interpretation speed; at high speeds over 0.001 the application may become unstable 118 | 119 | - `Bitness` — we can divide the binary code into words of 8 or 16 bits, which changes the number of available frequencies (256 or 65536) 120 | 121 | - `Panner` — pan between left (-1) and right (1) channels 122 | 123 | - `Frequency generation mode` 124 | 125 | - `continuous` — continuous frequency distribution 126 | - `tempered` — distribution by 12-step equal-tempered scale. There are notes from C-2 to B8 127 | 128 | - `Transition type` — transition between frequencies 129 | 130 | - `immediate` — instantaneous, rough transition 131 | - `linear` — linearly to the next frequency 132 | - `exponential` — exponentially to the next frequency 133 | 134 | - `Random time gap` — adds a random amount of time to the next tone within the `reading speed` parameter. Makes the sound less "robotic", as the distance to each tone is slightly different and it adds more "liveliness" to the playing 135 | 136 | - `Fragment` — allows to play not the whole file, but a certain part of it 137 | 138 | - `Solid mode` — the "solid press" mode, does not send `noteOff` commands; if the commands are the same in a row (and as a consequence notes), even noteOn is not sent. `allSoundOff` is sent at the end. On some synthesizers it allows smooth transitions between notes 139 | 140 | - `Last noteOn mode` — leaves the last command in the loop pressed. Allows to make smoother transitions between repeats of patterns. 141 | 142 | - Some input fields have a keyboard shortcut: pressing the corresponding key automatically moves the focus to the item. By pressing a key and moving the mouse at the same time, the values can be changed smoothly. Pressing `Shift` will increase (10x, 100x, 1000x) the "power" of the value change, pressing `Ctrl` will decrease (0.1x, 0.01x, 0.001x). The cursor disappears in order to be able to change values indefinitely. To return the cursor, press `Esc`. 143 | 144 | ## Saving settings 145 | 146 | 147 | You can save your settings in two ways: 148 | 149 | - via a URL link. When you click in the address bar of your browser, the application automatically generates a link to which the settings are written. You can copy it and when you open the link, the settings will be applied immediately, all you have to do is download a file for sound synthesis. 150 | - through a file. The interface includes Save / Load / Restore settings buttons, which allow you to save or load a settings file to or from your computer and also restore initial settings. 151 | 152 | ## Run locally and build the project 153 | 154 | 155 | ### Just copy the app 156 | 157 | Everything you need for the system is contained in a single `.html` file, which you can download in the `dist` folder, or simply go to https://bs.ѕtranno.su and right-click and select Save As in the menu. 158 | 159 | ### Build locally to work with the code 160 | 161 | Tech stack: Vue3 + Pinia + Vite. 162 | 163 | 1. Download and install the LTS version of Node.js 164 | 2. Download the code directly from Github, or via `git clone`. 165 | 3. In the project folder in the terminal execute: 166 | 167 | ```bash 168 | npm i 169 | npm run dev # development-build 170 | npm run build # production-build, generate index.html with everything we need 171 | ``` 172 | 173 | For MIDI tests, you can use this resource https://studiocode.dev/midi-monitor/ 174 | -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # Binary Synth 2 | 3 | _Синтез аудио из двоичного кода любого файла_ 4 | 5 | [![Uptime Robot status](https://img.shields.io/uptimerobot/status/m795264551-bb4c959b31b6ff94b02f9545)](https://bs.stranno.su) [![Uptime Robot status](https://img.shields.io/uptimerobot/ratio/m795264551-bb4c959b31b6ff94b02f9545)](https://bs.stranno.su) 6 | 7 | **Демо**: https://bs.stranno.su 8 | 9 | **Видео**: https://youtu.be/5LMYiLwfvRg 10 | 11 | ![](https://store.stranno.su/bs/fuji.png) 12 | 13 | Веб-синтезатор, генерирующий звук из двоичного кода любых файлов. Может синтезировать звук прямо в браузере, либо быть генератором MIDI-сообщений во внешние устройства или DAW, превращая любой файл в партитуру. Весь код приложения написан на Javascript и вместе со всем необходимым упаковывается в один .html файл размером около 750kb. Синтезатору не нужен интернет, его можно скачать и запускать локально на любом устройстве где есть браузер. 14 | 15 | Приложение последовательно читает файл и за счёт высокой скорости чтения и случайной величины отклонения длительности чтения, мы можем получить достаточно непредсказуемую генерацию нюансов тембра, а при определённых настройках перейти в гранулярный синтез. 16 | 17 | Вы можете попробовать несколько примеров настроек инструмента, которые будут звучать примерно одинаково для большинства файлов: 18 | 19 | - [IDM](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22sine%22,%22gain%22:2.04,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:0,%22to%22:8},%22notesRange%22:{%22from%22:48,%22to%22:60},%22fragment%22:{%22from%22:0,%22to%22:5000},%22midiMode%22:false,%22biquadFilterFrequency%22:33.1,%22biquadFilterQ%22:112.4,%22LFO%22:{%22enabled%22:true,%22type%22:%22triangle%22,%22rate%22:34.5,%22depth%22:1},%22bitness%22:%2216%22,%22panner%22:-0.67,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:50,%22noMIDIPortsFound%22:true,%22velocity%22:120,%22solidMode%22:false,%22lastNoteOnMode%22:true}}) 20 | - [Ambient](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22triangle%22,%22gain%22:0.41,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:0,%22to%22:1},%22notesRange%22:{%22from%22:48,%22to%22:104},%22fragment%22:{%22from%22:0,%22to%22:624},%22midiMode%22:false,%22biquadFilterFrequency%22:39.3,%22biquadFilterQ%22:121.3,%22LFO%22:{%22enabled%22:true,%22type%22:%22sine%22,%22rate%22:99,%22depth%22:0.395},%22bitness%22:%228%22,%22panner%22:0,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:66,%22noMIDIPortsFound%22:true,%22velocity%22:66,%22solidMode%22:true,%22lastNoteOnMode%22:true}}) 21 | - [Harsh noise](https://bs.stranno.su/#{%22readingSpeed%22:0.0001,%22waveType%22:%22sine%22,%22gain%22:2.56,%22transitionType%22:%22immediately%22,%22frequencyMode%22:%22continuous%22,%22frequenciesRange%22:{%22from%22:41,%22to%22:90},%22notesRange%22:{%22from%22:48,%22to%22:104},%22fragment%22:{%22from%22:0,%22to%22:100720},%22midiMode%22:false,%22biquadFilterFrequency%22:418.5,%22biquadFilterQ%22:66.6,%22LFO%22:{%22enabled%22:true,%22type%22:%22sine%22,%22rate%22:91,%22depth%22:0.546},%22bitness%22:%228%22,%22panner%22:0.15,%22loop%22:true,%22isRandomTimeGap%22:true,%22midi%22:{%22port%22:null,%22channel%22:0,%22pitch%22:8192,%22modulation%22:66,%22noMIDIPortsFound%22:true,%22velocity%22:66,%22solidMode%22:true,%22lastNoteOnMode%22:true}}) 22 | 23 | ## Содержание 24 | 25 | - [Принцип работы](#principle) 26 | - [Переход в гранулярный режим](#granular) 27 | - [Рекомендации по оптимальной работе](#recommendation) 28 | - [MIDI](#midi) 29 | - [Интерфейс](#interface) 30 | - [Сохранение настроек](#settings) 31 | - [Запуск локально и сборка проекта](#run) 32 | 33 | ## Принцип работы 34 | 35 | 36 | Все данные на любом компьютере или смартфоне представлены в виде файлов (являющихся, по своей сути, текстами). Содержанием этих файлов в конечном итоге являются просто нули и единицы. И эти нули и единицы, в общем-то, все одинаковые, поэтому нам нужен интерпретатор, для того чтобы извлечь смысл из этих текстов. Можно сказать, что формат файла (.mp3, .docx и т.д.) это просто указатель, какому интерпретатору надо передать текст, чтобы из него извлечь смысл. 37 | 38 | Но что, если формат файла и интерпретатор не совпадают? 39 | 40 | Что касается музыкальных экспериментов, то ранее были, например, попытки "воспроизвести" текстовый или иной файл через аудио-редактор. 41 | 42 | Мы могли бы пойти дальше и написать собственный интерпретатор, который смотрел бы на файлы безотносительно формата, использовал собственную "манеру чтения" исходных нулей и единиц и на этой основе предоставлял полноценную систему управляемого синтеза звуков. 43 | 44 | 1. Мы можем интерпретировать файлы как массив чисел. То есть, мы разбиваем непрерывный машинный код на _слова_ некоторой информационной ёмкости (разрядности): 45 | 46 | - 8 бит (числа от 0 до 255) 47 | - 16 бит (числа от 0 до 65 535) 48 | 49 | 2. Тогда, каждое слово есть команда, определяющая частоту звука 50 | 51 | 3. На уровне всей системы мы задаём глобальные параметры: 52 | 53 | - скорость интерпретации 54 | - наличие случайной величины разброса скорости интерпретации 55 | - музыкальный строй (или его отсутствие), диапазон нот/частот; по этому диапазону равномерно сопоставляются частоты по 256 или 65 536 возможным комбинациям нулей и единиц 56 | - зацикленность воспроизведения 57 | - режим MIDI 58 | - плавный или резкий переход между командами 59 | - настройки виртуальных устройств, необходимых для синтеза (осциллятор, фильтр, LFO), либо настройки MIDI 60 | 61 | 4. Чтобы снизить нагрузку на устройство, делим файл на куски по 500 команд 62 | 63 | 5. Рекурсивно планируем управление синтезом, читая по 500 команд в итерации и используя глобальные параметры 64 | 65 | 6. Если дошли до конца файла, прекращаем исполнение, либо начинаем заново 66 | 67 | ## Переход в гранулярный режим 68 | 69 | 70 | > **Note**: Здесь и далее используются термины интерфейса инструмента. Их описание смотрите ниже в разделе Особенности интерфейса 71 | 72 | Гранулярный синтез оперирует мелкими кусочками звуков — акустическими пикселями. Принято считать, что гранулярный синтез "начинается" при оперировании звуками <50мс. При значениях `fragment` * `reading speed` = <50 мы начинаем оперировать уже акустическими пикселями. 73 | 74 | При этом каждую команду из `fragment` можно считать "субпикселем", который, при включённом `random time gap`, каждый раз уникален, а пиксель, соответственно, уникален кратно количеству субпикселей. В итоге мы получаем мутирующий тембр. 75 | 76 | В классическом гранулярном синтезе пиксели играют одновременно и параллельно, и их количество может меняться со временем. В BS же пиксели образуют нить, по которой мы движемся. 77 | 78 | *То есть, если в обычном гранулярном синтезе на слушателя как бы опрокидывают грузовик с песком, где каждая песчинка это акустический пиксель, то здесь этот песок высыпается через воронку диаметром с одну песчинку и вот эту тонкую струю мы и наблюдаем.* 79 | 80 | На изображении ниже показано формирование акустического пикселя из двух команд (`fragment: from = 0, to = 1`), при скорости чтения 0.005 с. Необходимо учитывать, что у каждой частоты есть период *T*, равный отношению единицы времени (1 секунды = 1000 миллисекунд) к частоте. Это значит, что мы можем мыслить звук не только через частоту, но и через время, за которое волна делает одно полное колебание. Если волна не успевает сделать полное колебание, такой объект называется "вейвлетом". 81 | 82 | ![](https://store.stranno.su/bs/granular.jpg) 83 | 84 | ## Рекомендации по оптимальной работе 85 | 86 | 87 | - Использовать режим инкогнито при отключённых расширениях 88 | - Браузер не-инкогнито закрыть 89 | - В инкогнито оставить только вкладки с BS 90 | - Использовать отдельный браузер [ungoogled-chromium](https://github.com/ungoogled-software/ungoogled-chromium), потребляющий немного меньше CPU и сильно меньше ОЗУ. 91 | 92 | BS под нагрузкой требует в среднем до 7.1% CPU, в режиме инкогнито 6%, Firefox 4.2%, но работает менее стабильно. Также открытая консоль/DevTools браузера повышает потребление CPU на каждой вкладке на 10%. Рекомендуется использовать BS в режиме инкогнито без любых других открытых вкладок, кроме вкладок BS, для максимальной эффективности. 93 | 94 | Более интересный звук получается при нескольких независимых экземплярах BS в разных вкладках. Теоретически, можно было бы реализовать несколько потоков работы BS в одной вкладке, но это менее оптимально, так как браузеры ограничивают максимальное потребление CPU на вкладку (в Chrome это 10% CPU). Также, у каждой вкладки свой [event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop). Для быстрого переключения между вкладками можно использовать `ctrl + номер вкладки`. 95 | 96 | ## MIDI 97 | 98 | 99 | При включении MIDI-режима автоматически выбирается первый попавшийся порт из доступных и его первый канал. Далее последовательно при чтении посылается сигнал `noteOn`, через время `reading speed` посылается сигнал `noteOff`. В `continuous` режиме после каждого `noteOn` посылается `Pitch` сигнал, чтобы попасть в нужную частоту. 100 | 101 | MIDI-сообщения могут посылаться: 102 | 103 | - в соседние вкладки и окна браузеров, если они слушают MIDI (например, в веб-аналог [DX7](http://mmontag.github.io/dx7-synth-js)) 104 | - в DAW и прочие приложения, где есть виртуальные синтезаторы (то есть BS может управлять, например, синтезатором в Ableton) 105 | - во внешние устройства, поддерживающие MIDI и подключённые к компьютеру 106 | 107 | Для передачи MIDI-сообщений в DAW на устройствах Windows можно воспользоваться [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). 108 | 109 | > **Note**: После любых манипуляций с MIDI-портами (подключение/отключение/переподключение) необходимо полностью перезапустить браузер, закрыв все окна браузера если их несколько 110 | 111 | > **Note**: MIDI-сообщения генерируются только на десктопе 112 | 113 | ## Интерфейс 114 | 115 | 116 | - `Reading speed` — скорость интерпретации; на высоких скоростях более 0.001 приложение может работать нестабильно 117 | 118 | - `Bitness` — мы можем разделить двоичный код на слова по 8 или 16 бит, что меняет количество доступных частот (256 или 65536) 119 | 120 | - `Panner` — панорамирование между левым (-1) и правым (1) каналами 121 | 122 | - `Frequency generation mode` 123 | 124 | - `continuous` — непрерывное распределение частот 125 | - `tempered` — распределение по 12-ступенному равномерно-темперированному строю. Доступны ноты от C-2 до B8 126 | 127 | - `Transition type` — переход между частотами 128 | 129 | - `immediately` — моментально, грубый переход 130 | - `linear` — линейно до следующей частоты 131 | - `exponential` — экспоненциально до следующей частоты 132 | 133 | - `Random time gap` — добавление случайной величины времени до следующего звука в пределах параметра `reading speed`. Делает звук менее "роботизированным", так как расстояние до каждого звука немного отличается и это добавляет больше "живости" игре 134 | 135 | - `Fragment` — позволяет играть не весь файл, а его определённую часть 136 | 137 | - `Solid Mode` — режим "сплошного нажатия", не посылает команды `noteOff`; если подряд идут одинаковые команды (и как следствие ноты), то даже noteOn не посылается. В конце посылается `allSoundOff`. На некоторых синтезаторах позволять осуществить плавные переходы между нотами 138 | 139 | - `Last noteOn mode` — оставляет нажатой последнюю команду в лупе. Позволяет делать более плавные переходы между повторами паттернов. 140 | 141 | - У некоторых полей ввода есть клавиатурное сокращение: при нажатии соответствующей клавиши автоматически наводится фокус на элемент. При зажатии клавиши и одновременном движении мышью можно плавно менять значения. При нажатии Shift можно увеличить (10x, 100x, 1000x) "мощность" изменения значения, при нажатии на Ctrl уменьшить (0.1x, 0.01x, 0.001x). Курсор при этом пропадает, чтобы иметь возможность бесконечно изменять значения. Для возврата курсора необходимо нажать Esc. 142 | 143 | ## Сохранение настроек 144 | 145 | 146 | Сохранить настройки можно двумя путями: 147 | 148 | - через URL-ссылку. При щелчке в адресную строку браузера приложение автоматически формирует ссылку, в которую записываются настройки. Вы можете скопировать её и при открытии ссылки настройки сразу применятся, вам остаётся только загрузить файл для синтеза звука. 149 | - через файл. В интерфейсе преусмотрены кнопки Save / Load / Restore settings, которые позволяют сохранить на компьютер или загрузить из него файл с настройками, а также восстановить исходные. 150 | 151 | ## Запуск локально и сборка проекта 152 | 153 | 154 | ### Просто скопировать приложение 155 | 156 | Всё необходимое для работы системы заложено в единственный `.html` файл, который можно скачать в папке `dist`, либо просто перейти на https://bs.strannо.su и, нажав правую кнопку мыши, в меню выбрать Сохранить как. 157 | 158 | ### Собрать билд локально для доработки кода 159 | 160 | Tech stack: Vue3 + Pinia + Vite. 161 | 162 | 1. Скачать и установить LTS версию Node.js 163 | 2. Скачать код напрямую с Github, либо через `git clone` 164 | 3. В папке с проектом в терминале выполнить: 165 | 166 | ```bash 167 | npm i 168 | npm run dev # development-сборка 169 | npm run build # production-сборка, генерирует index.html со всем необходимым 170 | ``` 171 | 172 | Для тестов MIDI можно пользоваться этим ресурсом https://studiocode.dev/midi-monitor/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-synth", 3 | "version": "1.11.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --host", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "pinia": "^2.1.4", 12 | "vue": "^3.3.4", 13 | "worker-timers": "^8.0.2" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^4.2.3", 17 | "sass": "^1.64.2", 18 | "vite": "^4.4.6", 19 | "vite-plugin-mkcert": "^1.16.0", 20 | "vite-plugin-singlefile": "^0.13.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxAlyokhin/binary-synth/785260ea18e12edf35ac92b202261764a337c803/public/favicon.ico -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxAlyokhin/binary-synth/785260ea18e12edf35ac92b202261764a337c803/public/og.png -------------------------------------------------------------------------------- /src/BinarySynth.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 68 | -------------------------------------------------------------------------------- /src/assets/js/fourierCoefficients.js: -------------------------------------------------------------------------------- 1 | export default { 2 | square: { 3 | real: [ 4 | -0.36626994609832764, 6.029391288757324, -0.19730372726917267, 1.6619471311569214, -0.19724227488040924, 1.3105599880218506, 5 | -0.1978960931301117, 1.2136590480804443, -0.1982928365468979, 1.173160195350647, -0.19904087483882904, 1.151987910270691, 6 | -0.1997050791978836, 1.138441801071167, -0.20070235431194305, 1.1281323432922363, -0.2019263505935669, 1.1190999746322632, 7 | -0.20291641354560852, 1.1103429794311523, -0.2038358747959137, 1.1008262634277344, -0.2046438753604889, 1.090717077255249, 8 | -0.20541562139987946, 1.0797215700149536, -0.20619842410087585, 1.0680949687957764, -0.20637062191963196, 1.055716872215271, 9 | -0.20673519372940063, 1.0433330535888672, -0.20701037347316742, 1.0309003591537476, -0.20660743117332458, 1.0185457468032837, 10 | -0.205982968211174, 1.0071656703948975, -0.20559249818325043, 0.995996356010437, -0.20450708270072937, 0.9865143895149231, 11 | -0.2038058489561081, 0.9773967862129211, -0.20271623134613037, 0.9703430533409119, -0.20159627497196198, 0.9642654061317444, 12 | -0.20063833892345428, 0.9593200087547302, -0.19978035986423492, 0.9556630253791809, -0.19863766431808472, 0.9527756571769714, 13 | -0.1979074627161026, 0.9507238864898682, -0.1973714679479599, 0.9496198296546936, -0.19705352187156677, 0.9490557909011841, 0, 14 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 | ], 17 | imag: [ 18 | 0, 61.61503982543945, -0.0070415339432656765, 20.611003875732422, -0.003715433878824115, 12.320550918579102, 19 | -0.003177450504153967, 8.746326446533203, -0.002948831068351865, 6.746776580810547, -0.002961810678243637, 5.462915897369385, 20 | -0.002885965397581458, 4.565426826477051, -0.0030379416421055794, 3.8996381759643555, -0.0029344737995415926, 21 | 3.3834776878356934, -0.0031999927014112473, 2.9704341888427734, -0.0033238078467547894, 2.630431890487671, 22 | -0.003493582597002387, 2.3441388607025146, -0.0035432178992778063, 2.0985631942749023, -0.003574345028027892, 23 | 1.8846455812454224, -0.0035537832882255316, 1.6950992345809937, -0.0035966038703918457, 1.5251586437225342, 24 | -0.0035648683551698923, 1.3712939023971558, -0.003324032761156559, 1.2298352718353271, -0.003059905022382736, 1.099546194076538, 25 | -0.002998646115884185, 0.9782153964042664, -0.002375461161136627, 0.864102840423584, -0.0023680778685957193, 0.7568547129631042, 26 | -0.0022333727683871984, 0.655020534992218, -0.0018261834047734737, 0.5579419732093811, -0.0015898228157311678, 27 | 0.4648510217666626, -0.0011751690180972219, 0.3757583796977997, -0.000904021377209574, 0.2892845571041107, 28 | -0.0009147493401542306, 0.20495030283927917, -0.0003731999604497105, 0.12234722077846527, 0.00006776519876439124, 29 | 0.04073695093393326, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31 | ], 32 | }, 33 | sawtooth: { 34 | real: [ 35 | -0.2128259390592575, 0.43020427227020264, -0.3802155554294586, 0.39318570494651794, -0.3866429328918457, 0.3911486864089966, 36 | -0.38942503929138184, 0.3925042748451233, -0.3924199342727661, 0.3953993022441864, -0.39608505368232727, 0.39898601174354553, 37 | -0.40029671788215637, 0.40276744961738586, -0.40471380949020386, 0.40744227170944214, -0.40929925441741943, 0.41191843152046204, 38 | -0.41422832012176514, 0.41683557629585266, -0.4191102087497711, 0.42197152972221375, -0.42425692081451416, 0.42680537700653076, 39 | -0.4291529059410095, 0.43171635270118713, -0.43399283289909363, 0.4365193843841553, -0.43843600153923035, 0.44087883830070496, 40 | -0.4428611397743225, 0.4448528289794922, -0.4468531012535095, 0.44867464900016785, -0.45029324293136597, 0.45193761587142944, 41 | -0.4536658525466919, 0.45539727807044983, -0.45665937662124634, 0.4575652480125427, -0.45892730355262756, 0.45996278524398804, 42 | -0.46089065074920654, 0.4617873430252075, -0.4625128507614136, 0.46289584040641785, -0.4634133279323578, 0.4640624523162842, 43 | -0.46411240100860596, 0.4642360806465149, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45 | ], 46 | imag: [ 47 | 0, 40.303253173828125, -15.415637969970703, 11.028034210205078, -7.9055070877075195, 6.439881324768066, -5.254321575164795, 48 | 4.5257697105407715, -3.900256395339966, 3.4624736309051514, -3.0736374855041504, 2.7797441482543945, -2.5129919052124023, 49 | 2.300569534301758, -2.104926824569702, 1.9431331157684326, -1.7928383350372314, 1.6648855209350586, -1.5446336269378662, 50 | 1.4402865171432495, -1.3418267965316772, 1.2545875310897827, -1.1717488765716553, 1.0972166061401367, -1.0264544486999512, 51 | 0.9616853594779968, -0.8999052047729492, 0.843029260635376, -0.7885620594024658, 0.7378413677215576, -0.6889731884002686, 52 | 0.6428762674331665, -0.5986546874046326, 0.5569077730178833, -0.5164836645126343, 0.477679044008255, -0.4402264356613159, 53 | 0.4043141007423401, -0.3690325915813446, 0.33509474992752075, -0.3017183244228363, 0.26975053548812866, -0.23788419365882874, 54 | 0.20691440999507904, -0.1764654815196991, 0.14642955362796783, -0.11661481112241745, 0.08744602650403976, -0.05802077800035477, 55 | 0.02892916277050972, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57 | ], 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /src/assets/js/getFrequency.js: -------------------------------------------------------------------------------- 1 | import { notes } from './notes.js' 2 | 3 | export function getFrequency(byte, bitness, mode, coefficients, minimumFrequency, minimumNote) { 4 | if (mode === 'continuous') { 5 | if (byte === 0) return 0.01 + minimumFrequency 6 | if (bitness === '8') return coefficients.continouos8 * byte + minimumFrequency 7 | if (bitness === '16') return coefficients.continouos16 * byte + minimumFrequency 8 | } 9 | 10 | if (mode === 'tempered') { 11 | if (bitness === '8') return notes[Math.floor(coefficients.tempered8 * byte) + minimumNote] 12 | if (bitness === '16') return notes[Math.floor(coefficients.tempered16 * byte) + minimumNote] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/js/getMIDINote.js: -------------------------------------------------------------------------------- 1 | import { notes } from './notes.js' 2 | import { toFixedNumber } from './helpers.js' 3 | 4 | /** 5 | * The function searches for the nearest lower and nearest higher number to a given number 6 | * @param {Number} number - the number around which we need to find the nearest values 7 | * @param {Array} array - an array of numbers from which we select the nearest values 8 | * @return {Array} Returns an array of two numbers: a smaller and a larger one 9 | */ 10 | 11 | let nearbyLess = null 12 | let nearbyOver = null 13 | function getNearbyValues(number, array) { 14 | nearbyLess = Math.max(...array.filter((value) => value < number)) 15 | isFinite(nearbyLess) ? nearbyLess : (nearbyLess = 0) 16 | nearbyOver = Math.min(...array.filter((value) => value > number)) 17 | 18 | return [nearbyLess, nearbyOver] 19 | } 20 | 21 | // Calculates an array of: note number and, in continuous mode, pitch value 22 | let frequency = null 23 | let nearbyValues = null 24 | let percent = null 25 | let pitchValue = null 26 | 27 | export function getMIDINote(byte, bitness, mode, coefficients, minimumFrequency, minimumNote) { 28 | // Note number + pitch is returned 29 | // 1. Calculate frequency 30 | // 2. Find the nearest lower note in the array to this frequency 31 | // 3. Calculate the difference between this note and the original frequency 32 | // 4. Convert this difference into a pitch value 33 | if (mode === 'continuous') { 34 | // 1. 35 | if (byte === 0) frequency = minimumFrequency 36 | if (bitness === '8') frequency = coefficients.continouos8 * byte + minimumFrequency 37 | if (bitness === '16') frequency = coefficients.continouos16 * byte + minimumFrequency 38 | 39 | // 2. 40 | nearbyValues = getNearbyValues(frequency, notes) 41 | 42 | // 3. 43 | percent = toFixedNumber(((frequency - nearbyValues[0]) / (nearbyValues[1] - nearbyValues[0])) * 100, 1) 44 | 45 | // 4. 46 | // The pitch value in MIDI is from 0 to 16383, 8191 is the normal state (middle) 47 | // 8192 divisions are two semitones, so one semitone is 4096 divisions 48 | // We want to make a smooth transition between halftones, so we need to define a shift up to 4096 49 | pitchValue = Math.floor((percent / 100) * 4096) + 8191 50 | 51 | if (notes.indexOf(nearbyValues[0]) < 0) { 52 | return [0, pitchValue] 53 | } else { 54 | return [notes.indexOf(nearbyValues[0]), pitchValue] 55 | } 56 | } 57 | 58 | // The note number returned 59 | if (mode === 'tempered') { 60 | if (bitness === '8') return [Math.floor(coefficients.tempered8 * byte) + minimumNote] 61 | if (bitness === '16') return [Math.floor(coefficients.tempered16 * byte) + minimumNote] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/js/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The function rounds the value to 3 decimal places by default 3 | * @param {Number} number - number 4 | * @param {Number} digits - number of decimal places 5 | * @return {Number} Returns a rounded number 6 | */ 7 | 8 | let pow = null 9 | export function toFixedNumber(number, digits = 3) { 10 | if (number) { 11 | pow = Math.pow(10, digits) 12 | return Math.round(number * pow) / pow 13 | } else { 14 | return 0 15 | } 16 | } 17 | 18 | export function getRandomNumber(min, max) { 19 | return Math.random() * (max - min) + min 20 | } 21 | 22 | /** 23 | * Converts 'true' || 'false' strings to the corresponding boolean value 24 | * @param {String} value - conversion string 25 | * @return {Boolean} Returns a boolean value 26 | */ 27 | export function getBooleanFromString(value) { 28 | // prettier-ignore 29 | return value === 'true' 30 | ? true 31 | : false 32 | } 33 | 34 | /** 35 | * The function performs integer division 36 | * @param {Number} value - what to divide 37 | * @param {Number} by - by what 38 | * @return {Number} Returns the number you are looking for 39 | */ 40 | 41 | export const div = (value, by) => (value - (value % by)) / by 42 | 43 | /** 44 | * The function performs a count of digits after comma 45 | * @param {Number} x - Number 46 | * @return {Number} Returns the number of digits after comma 47 | */ 48 | 49 | export function decimalPlaces(x) { 50 | return x.toString().includes('.') ? x.toString().split('.').pop().length : 0 51 | } 52 | 53 | /** 54 | * The function returns a string with the time of the call in the format number-month-year-hour-minutes-seconds 55 | * @return {String} 56 | */ 57 | 58 | export function getDate() { 59 | let date = new Date() 60 | return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}` 61 | } -------------------------------------------------------------------------------- /src/assets/js/midiMessages.js: -------------------------------------------------------------------------------- 1 | // MIDI messages 2 | 3 | export default { 4 | noteOff(note, velocity, port, channel) { 5 | port.send([0x80 + Number(channel), note, velocity]) 6 | }, 7 | 8 | noteOn(note, velocity, port, channel) { 9 | port.send([0x90 + Number(channel), note, velocity]) 10 | }, 11 | 12 | pitch(value, port, channel) { 13 | port.send([0xe0 + Number(channel), value & 0x7f, value >> 7]) 14 | }, 15 | 16 | allSoundOff(port, channel) { 17 | port.send([0xb0 + Number(channel), 0x78, 0]) 18 | }, 19 | 20 | modulation(value, port, channel) { 21 | port.send([0xb0 + Number(channel), 0x01, value]) 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/js/notes.js: -------------------------------------------------------------------------------- 1 | import { div } from './helpers' 2 | 3 | export const notes = [ 4 | 8.176, 8.662, 9.177, 9.723, 10.301, 10.913, 11.562, 12.25, 12.978, 13.75, 14.567, 15.434, 16.351, 17.324, 18.354, 19.445, 20.601, 5 | 21.827, 23.124, 24.499, 25.956, 27.5, 29.135, 30.867, 32.703, 34.647, 36.708, 38.89, 41.203, 43.653, 46.249, 48.999, 51.912, 54.999, 6 | 58.27, 61.735, 65.406, 69.295, 73.415, 77.781, 82.406, 87.306, 92.497, 97.998, 103.825, 109.999, 116.54, 123.469, 130.811, 138.59, 7 | 146.831, 155.562, 164.812, 174.612, 184.995, 195.995, 207.65, 219.997, 233.079, 246.939, 261.622, 277.179, 293.661, 311.123, 329.624, 8 | 349.224, 369.99, 391.991, 415.3, 439.995, 466.158, 493.877, 523.245, 554.359, 587.322, 622.246, 659.247, 698.448, 739.98, 783.981, 9 | 830.599, 879.989, 932.316, 987.755, 1046.49, 1108.717, 1174.645, 1244.493, 1318.494, 1396.896, 1479.96, 1567.963, 1661.199, 1759.979, 10 | 1864.632, 1975.509, 2092.979, 2217.434, 2349.29, 2488.986, 2636.989, 2793.792, 2959.92, 3135.926, 3322.397, 3519.957, 3729.265, 11 | 3951.019, 4185.958, 4434.868, 4698.579, 4977.972, 5273.977, 5587.584, 5919.839, 6271.851, 6644.795, 7039.915, 7458.53, 7902.037, 12 | 8371.917, 8869.737, 9397.159, 9955.943, 10547.954, 11175.168, 11839.678, 12543.702, 13289.59, 14079.83, 14917.06, 15804.074, 13 | ] 14 | 15 | /** 16 | * Defines the name of the note through the frequency 17 | * @param {Number} frequency - frequency 18 | * @return {String} noteName - note name 19 | */ 20 | 21 | const notesNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] 22 | export function getNoteName(noteID) { 23 | // Octave No. 24 | const octave = div(noteID, 12) - 1 25 | // The order number of the note within an octave 26 | // For example, D === 3 (C - C# - D) 27 | const noteNumberOnOctave = noteID + 1 - 12 * (octave + 1) 28 | // Assemble the name of the note together with the octave No. 29 | const noteName = notesNames[noteNumberOnOctave - 1] + String(octave - 1) 30 | 31 | return noteName 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | html, 17 | body { 18 | height: 100%; 19 | } 20 | 21 | /* Sections 22 | ========================================================================== */ 23 | 24 | /** 25 | * Remove the margin in all browsers. 26 | */ 27 | 28 | body { 29 | margin: 0; 30 | } 31 | 32 | /** 33 | * Render the `main` element consistently in IE. 34 | */ 35 | 36 | main { 37 | display: block; 38 | } 39 | 40 | /** 41 | * Correct the font size and margin on `h1` elements within `section` and 42 | * `article` contexts in Chrome, Firefox, and Safari. 43 | */ 44 | 45 | h1 { 46 | font-size: 2em; 47 | margin: 0.67em 0; 48 | } 49 | 50 | /* Grouping content 51 | ========================================================================== */ 52 | 53 | /** 54 | * 1. Add the correct box sizing in Firefox. 55 | * 2. Show the overflow in Edge and IE. 56 | */ 57 | 58 | hr { 59 | box-sizing: content-box; /* 1 */ 60 | height: 0; /* 1 */ 61 | overflow: visible; /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; /* 1 */ 71 | font-size: 1em; /* 2 */ 72 | } 73 | 74 | /* Text-level semantics 75 | ========================================================================== */ 76 | 77 | /** 78 | * Remove the gray background on active links in IE 10. 79 | */ 80 | 81 | a { 82 | background-color: transparent; 83 | } 84 | 85 | /** 86 | * 1. Remove the bottom border in Chrome 57- 87 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 88 | */ 89 | 90 | abbr[title] { 91 | border-bottom: none; /* 1 */ 92 | text-decoration: underline; /* 2 */ 93 | text-decoration: underline dotted; /* 2 */ 94 | } 95 | 96 | /** 97 | * Add the correct font weight in Chrome, Edge, and Safari. 98 | */ 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | /** 106 | * 1. Correct the inheritance and scaling of font size in all browsers. 107 | * 2. Correct the odd `em` font sizing in all browsers. 108 | */ 109 | 110 | code, 111 | kbd, 112 | samp { 113 | font-family: monospace, monospace; /* 1 */ 114 | font-size: 1em; /* 2 */ 115 | } 116 | 117 | /** 118 | * Add the correct font size in all browsers. 119 | */ 120 | 121 | small { 122 | font-size: 80%; 123 | } 124 | 125 | /** 126 | * Prevent `sub` and `sup` elements from affecting the line height in 127 | * all browsers. 128 | */ 129 | 130 | sub, 131 | sup { 132 | font-size: 75%; 133 | line-height: 0; 134 | position: relative; 135 | vertical-align: baseline; 136 | } 137 | 138 | sub { 139 | bottom: -0.25em; 140 | } 141 | 142 | sup { 143 | top: -0.5em; 144 | } 145 | 146 | /* Embedded content 147 | ========================================================================== */ 148 | 149 | /** 150 | * Remove the border on images inside links in IE 10. 151 | */ 152 | 153 | img { 154 | border-style: none; 155 | } 156 | 157 | /* Forms 158 | ========================================================================== */ 159 | 160 | /** 161 | * 1. Change the font styles in all browsers. 162 | * 2. Remove the margin in Firefox and Safari. 163 | */ 164 | 165 | button, 166 | input, 167 | optgroup, 168 | select, 169 | textarea { 170 | font-family: inherit; /* 1 */ 171 | font-size: 100%; /* 1 */ 172 | line-height: 1.15; /* 1 */ 173 | margin: 0; /* 2 */ 174 | } 175 | 176 | /** 177 | * Show the overflow in IE. 178 | * 1. Show the overflow in Edge. 179 | */ 180 | 181 | button, 182 | input { 183 | /* 1 */ 184 | overflow: visible; 185 | } 186 | 187 | /** 188 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 189 | * 1. Remove the inheritance of text transform in Firefox. 190 | */ 191 | 192 | button, 193 | select { 194 | /* 1 */ 195 | text-transform: none; 196 | } 197 | 198 | /** 199 | * Correct the inability to style clickable types in iOS and Safari. 200 | */ 201 | 202 | button, 203 | [type='button'], 204 | [type='reset'], 205 | [type='submit'] { 206 | -webkit-appearance: button; 207 | } 208 | 209 | /** 210 | * Remove the inner border and padding in Firefox. 211 | */ 212 | 213 | button::-moz-focus-inner, 214 | [type='button']::-moz-focus-inner, 215 | [type='reset']::-moz-focus-inner, 216 | [type='submit']::-moz-focus-inner { 217 | border-style: none; 218 | padding: 0; 219 | } 220 | 221 | /** 222 | * Restore the focus styles unset by the previous rule. 223 | */ 224 | 225 | button:-moz-focusring, 226 | [type='button']:-moz-focusring, 227 | [type='reset']:-moz-focusring, 228 | [type='submit']:-moz-focusring { 229 | outline: 1px dotted ButtonText; 230 | } 231 | 232 | /** 233 | * Correct the padding in Firefox. 234 | */ 235 | 236 | fieldset { 237 | padding: 0.35em 0.75em 0.625em; 238 | } 239 | 240 | /** 241 | * 1. Correct the text wrapping in Edge and IE. 242 | * 2. Correct the color inheritance from `fieldset` elements in IE. 243 | * 3. Remove the padding so developers are not caught out when they zero out 244 | * `fieldset` elements in all browsers. 245 | */ 246 | 247 | legend { 248 | box-sizing: border-box; /* 1 */ 249 | color: inherit; /* 2 */ 250 | display: table; /* 1 */ 251 | max-width: 100%; /* 1 */ 252 | padding: 0; /* 3 */ 253 | white-space: normal; /* 1 */ 254 | } 255 | 256 | /** 257 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 258 | */ 259 | 260 | progress { 261 | vertical-align: baseline; 262 | } 263 | 264 | /** 265 | * Remove the default vertical scrollbar in IE 10+. 266 | */ 267 | 268 | textarea { 269 | overflow: auto; 270 | } 271 | 272 | /** 273 | * 1. Add the correct box sizing in IE 10. 274 | * 2. Remove the padding in IE 10. 275 | */ 276 | 277 | [type='checkbox'], 278 | [type='radio'] { 279 | box-sizing: border-box; /* 1 */ 280 | padding: 0; /* 2 */ 281 | } 282 | 283 | /** 284 | * Correct the cursor style of increment and decrement buttons in Chrome. 285 | */ 286 | 287 | [type='number']::-webkit-inner-spin-button, 288 | [type='number']::-webkit-outer-spin-button { 289 | height: auto; 290 | } 291 | 292 | /** 293 | * 1. Correct the odd appearance in Chrome and Safari. 294 | * 2. Correct the outline style in Safari. 295 | */ 296 | 297 | [type='search'] { 298 | -webkit-appearance: textfield; /* 1 */ 299 | outline-offset: -2px; /* 2 */ 300 | } 301 | 302 | /** 303 | * Remove the inner padding in Chrome and Safari on macOS. 304 | */ 305 | 306 | [type='search']::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | /** 311 | * 1. Correct the inability to style clickable types in iOS and Safari. 312 | * 2. Change font properties to `inherit` in Safari. 313 | */ 314 | 315 | ::-webkit-file-upload-button { 316 | -webkit-appearance: button; /* 1 */ 317 | font: inherit; /* 2 */ 318 | } 319 | 320 | /* Interactive 321 | ========================================================================== */ 322 | 323 | /* 324 | * Add the correct display in Edge, IE 10+, and Firefox. 325 | */ 326 | 327 | details { 328 | display: block; 329 | } 330 | 331 | /* 332 | * Add the correct display in all browsers. 333 | */ 334 | 335 | summary { 336 | display: list-item; 337 | } 338 | 339 | /* Misc 340 | ========================================================================== */ 341 | 342 | /** 343 | * Add the correct display in IE 10+. 344 | */ 345 | 346 | template { 347 | display: none; 348 | } 349 | 350 | /** 351 | * Add the correct display in IE 10. 352 | */ 353 | 354 | [hidden] { 355 | display: none; 356 | } 357 | -------------------------------------------------------------------------------- /src/assets/styles/vars.scss: -------------------------------------------------------------------------------- 1 | $white: rgb(201 209 204); 2 | $black: rgb(13, 17, 23); 3 | $grey: #171b22; 4 | $blue: rgb(165 214 255); 5 | $orange: rgb(238 143 39); 6 | $red: #ff0016; 7 | $green: #00ff53; 8 | -------------------------------------------------------------------------------- /src/components/ControlPanel/ControlPanel.vue: -------------------------------------------------------------------------------- 1 | 1173 | 1174 | 1194 | 1195 | 1228 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Filter.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 62 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Frequency.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 151 | 152 | 172 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Global.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 226 | -------------------------------------------------------------------------------- /src/components/ControlPanel/InteractiveInput.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 133 | 134 | 201 | -------------------------------------------------------------------------------- /src/components/ControlPanel/LFO.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 97 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Midi.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 190 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Oscillator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 50 | -------------------------------------------------------------------------------- /src/components/ControlPanel/SettingsExchange.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 96 | -------------------------------------------------------------------------------- /src/components/FileInput.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 146 | 147 | 247 | -------------------------------------------------------------------------------- /src/components/Status.vue: -------------------------------------------------------------------------------- 1 | 178 | 179 | 223 | 224 | 315 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/styles/main.scss' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | import BinarySynth from './BinarySynth.vue' 6 | 7 | createApp(BinarySynth).use(createPinia()).mount('#app') 8 | -------------------------------------------------------------------------------- /src/stores/globalStore.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useFileStore = defineStore('file', () => { 5 | const binary8 = ref(null) 6 | const binary16 = ref([]) 7 | const name = ref('') 8 | const size = ref(0) 9 | const type = ref('') 10 | const loaded = ref(false) 11 | 12 | return { binary8, binary16, name, size, type, loaded } 13 | }) 14 | 15 | export const useSettingsStore = defineStore('settings', () => { 16 | const readingSpeed = ref(0.01) 17 | const waveType = ref('sine') 18 | const gain = ref(1) 19 | const settingsFileName = ref('') 20 | const transitionType = ref('immediately') 21 | const frequencyMode = ref('continuous') 22 | const frequenciesRange = ref({ 23 | from: 50, 24 | to: 256, 25 | }) 26 | const notesRange = ref({ 27 | from: 36, 28 | to: 48, 29 | }) 30 | const fragment = ref({ 31 | from: 0, 32 | to: 0, 33 | }) 34 | const biquadFilterFrequency = ref(10000.0) 35 | const biquadFilterQ = ref(1) 36 | const LFO = ref({ 37 | enabled: false, 38 | type: 'sine', 39 | rate: 1, 40 | depth: 1, 41 | }) 42 | const bitness = ref('8') 43 | const panner = ref(0) 44 | const loop = ref(true) 45 | const isRandomTimeGap = ref(true) 46 | const midiMode = ref(false) 47 | const midi = ref({ 48 | port: null, 49 | channel: 0, 50 | pitch: 8192, 51 | modulation: 50, 52 | noMIDIPortsFound: true, 53 | velocity: 120, 54 | solidMode: false, 55 | lastNoteOnMode: true 56 | }) 57 | 58 | return { 59 | readingSpeed, 60 | waveType, 61 | gain, 62 | settingsFileName, 63 | transitionType, 64 | frequencyMode, 65 | frequenciesRange, 66 | notesRange, 67 | fragment, 68 | midiMode, 69 | biquadFilterFrequency, 70 | biquadFilterQ, 71 | LFO, 72 | bitness, 73 | panner, 74 | loop, 75 | isRandomTimeGap, 76 | midi, 77 | } 78 | }) 79 | 80 | export const useStatusStore = defineStore('status', () => { 81 | const playing = ref(false) 82 | const timer = ref(0) 83 | const startAndEndOfList = ref([0, 499]) 84 | const currentCommand = ref(0) 85 | const listID = ref(0) 86 | const isSettingsFileActual = ref(false) 87 | 88 | return { playing, timer, startAndEndOfList, currentCommand, listID, isSettingsFileActual } 89 | }) 90 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import { viteSingleFile } from 'vite-plugin-singlefile' 6 | import mkcert from 'vite-plugin-mkcert' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | server: { https: true }, 11 | plugins: [vue(), viteSingleFile({ removeViteModuleLoader: true }), mkcert()], 12 | resolve: { 13 | alias: { 14 | '@': fileURLToPath(new URL('./src', import.meta.url)), 15 | }, 16 | }, 17 | }) 18 | --------------------------------------------------------------------------------