├── public ├── og.png └── favicon.ico ├── src ├── assets │ ├── styles │ │ ├── vars.scss │ │ └── normalize.css │ └── js │ │ ├── midiMessages.js │ │ ├── getFrequency.js │ │ ├── notes.js │ │ ├── helpers.js │ │ ├── getMIDINote.js │ │ └── fourierCoefficients.js ├── main.js ├── composables │ ├── useKeyboardControl.js │ ├── useOscillatorScheduler.js │ ├── useMidiScheduler.js │ ├── useAudioGraph.js │ └── usePlaybackControl.js ├── components │ ├── ControlPanel │ │ ├── Oscillator.vue │ │ ├── Filter.vue │ │ ├── SampleRate.vue │ │ ├── LFO.vue │ │ ├── SettingsExchange.vue │ │ ├── ControlPanel.vue │ │ ├── InteractiveInput.vue │ │ ├── Frequency.vue │ │ ├── Midi.vue │ │ └── Global.vue │ ├── FileInput.vue │ └── Status.vue ├── BinarySynth.vue └── stores │ └── globalStore.js ├── .gitignore ├── vite.config.js ├── .prettierrc ├── package.json ├── LICENSE ├── index.html ├── README.md └── README_RU.md /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxAlyokhin/binary-synth/HEAD/public/og.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxAlyokhin/binary-synth/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 18 | .vscode 19 | .tool-versions -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-synth", 3 | "version": "1.16.3", 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 | -------------------------------------------------------------------------------- /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/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.continuous8 * byte + minimumFrequency 7 | if (bitness === '16') return coefficients.continuous16 * byte + minimumFrequency 8 | } 9 | 10 | if (mode === 'tempered') { 11 | if (bitness === '8') return notes[Math.floor(coefficients.tempered8 * byte) + Math.round(minimumNote)] 12 | if (bitness === '16') return notes[Math.floor(coefficients.tempered16 * byte) + Math.round(minimumNote)] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/composables/useKeyboardControl.js: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from 'vue' 2 | 3 | export function useKeyboardControl(status, play, stop) { 4 | function changePlaying(event) { 5 | if (event.code === 'Space') { 6 | status.playing ? stop() : play() 7 | } 8 | } 9 | 10 | function preventScrollOnSpacePress(event) { 11 | if (event.code === 'Space') event.preventDefault() 12 | } 13 | 14 | onMounted(() => { 15 | window.addEventListener('keydown', preventScrollOnSpacePress) 16 | window.addEventListener('keyup', changePlaying) 17 | }) 18 | 19 | onUnmounted(() => { 20 | window.removeEventListener('keydown', preventScrollOnSpacePress) 21 | window.removeEventListener('keyup', changePlaying) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Oscillator.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | -------------------------------------------------------------------------------- /src/BinarySynth.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 69 | -------------------------------------------------------------------------------- /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/components/ControlPanel/Filter.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 59 | -------------------------------------------------------------------------------- /src/composables/useOscillatorScheduler.js: -------------------------------------------------------------------------------- 1 | import { getFrequency } from '../assets/js/getFrequency.js' 2 | import { getRandomTimeGap } from '../assets/js/helpers.js' 3 | 4 | export function useOscillatorScheduler(settings, audioContext, oscillator, bynaryInSelectedBitness, frequencyCoefficients) { 5 | function computeFrequency(binaryValue) { 6 | return getFrequency( 7 | binaryValue, 8 | settings.bitness, 9 | settings.frequencyMode, 10 | frequencyCoefficients.value, 11 | settings.frequenciesRange.from, 12 | settings.notesRange.from 13 | ) 14 | } 15 | 16 | function scheduleOscillatorValue(command, targetTime) { 17 | // At high reading speeds, there are unacceptable values 18 | const isExponential = settings.transitionType === 'exponential' 19 | const safeValue = isFinite(command) ? command : isExponential ? 0.01 : 0 20 | 21 | switch (settings.transitionType) { 22 | case 'immediately': 23 | oscillator.value.frequency.setValueAtTime(safeValue, targetTime) 24 | break 25 | case 'linear': 26 | oscillator.value.frequency.linearRampToValueAtTime(safeValue, targetTime) 27 | break 28 | case 'exponential': 29 | oscillator.value.frequency.exponentialRampToValueAtTime(safeValue, targetTime) 30 | break 31 | } 32 | } 33 | 34 | function planOscillatorList(startOfList, endOfList) { 35 | for (let binaryID = startOfList, index = 0; binaryID <= endOfList; binaryID++, index++) { 36 | const command = computeFrequency(bynaryInSelectedBitness.value[binaryID]) 37 | const time = audioContext.value.currentTime + (index * settings.readingSpeed + getRandomTimeGap(settings.isRandomTimeGap, settings.readingSpeed)) 38 | scheduleOscillatorValue(command, time) 39 | } 40 | } 41 | 42 | return { 43 | getRandomTimeGap, 44 | computeFrequency, 45 | scheduleOscillatorValue, 46 | planOscillatorList, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | export function getRandomTimeGap(isRandomTimeGap, readingSpeed) { 23 | return isRandomTimeGap ? getRandomNumber(0, readingSpeed) : 0 24 | } 25 | 26 | /** 27 | * The function performs integer division 28 | * @param {Number} value - what to divide 29 | * @param {Number} by - by what 30 | * @return {Number} Returns the number you are looking for 31 | */ 32 | 33 | export const div = (value, by) => (value - (value % by)) / by 34 | 35 | /** 36 | * The function performs a count of digits after comma 37 | * @param {Number} x - Number 38 | * @return {Number} Returns the number of digits after comma 39 | */ 40 | 41 | export function decimalPlaces(x) { 42 | return x.toString().includes('.') ? x.toString().split('.').pop().length : 0 43 | } 44 | 45 | /** 46 | * The function returns a string with the time of the call in the format number-month-year-hour-minutes-seconds 47 | * @return {String} 48 | */ 49 | 50 | export function getDate() { 51 | let date = new Date() 52 | return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}` 53 | } 54 | 55 | /** 56 | * Check sample rate value 57 | * @param {Number} minimum - minimum value 58 | * @param {Number} maximum - maximum value 59 | * @param {Number} sampleRate - value for check 60 | * @return {Number} Returns the valid sample rate value 61 | */ 62 | 63 | export function checkSampleRate(minimum, maximum, sampleRate) { 64 | if (sampleRate <= maximum && sampleRate >= minimum) return sampleRate 65 | else if (sampleRate >= maximum) return maximum 66 | else if (sampleRate <= minimum) return minimum 67 | } 68 | 69 | /** 70 | * Check setting that is out of range with new sample rate 71 | * @param {Number} sampleRate - sampleRate 72 | * @param {Number} setting - setting for check 73 | * @return {Number} Returns the valid setting value 74 | */ 75 | 76 | export function checkFrequenciesWithNewSampleRate(sampleRate, setting) { 77 | return setting > sampleRate / 2 ? sampleRate / 2 : setting 78 | } 79 | -------------------------------------------------------------------------------- /src/components/ControlPanel/SampleRate.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /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.continuous8 * byte + minimumFrequency 37 | if (bitness === '16') frequency = coefficients.continuous16 * 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/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: 499, 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 | const sampleRate = ref(null) 58 | const sampleRateRange = ref({ 59 | maximum: null, 60 | minimum: null 61 | }) 62 | 63 | return { 64 | readingSpeed, 65 | waveType, 66 | gain, 67 | settingsFileName, 68 | transitionType, 69 | frequencyMode, 70 | frequenciesRange, 71 | notesRange, 72 | fragment, 73 | midiMode, 74 | biquadFilterFrequency, 75 | biquadFilterQ, 76 | LFO, 77 | bitness, 78 | panner, 79 | loop, 80 | isRandomTimeGap, 81 | midi, 82 | sampleRate, 83 | sampleRateRange, 84 | } 85 | }) 86 | 87 | export const useStatusStore = defineStore('status', () => { 88 | const playing = ref(false) 89 | const timer = ref(0) 90 | const startAndEndOfList = ref([0, 499]) 91 | const currentCommand = ref(0) 92 | const listID = ref(0) 93 | const currentIteration = ref(0) 94 | const isSettingsFileActual = ref(false) 95 | 96 | return { playing, timer, startAndEndOfList, currentCommand, listID, currentIteration, isSettingsFileActual } 97 | }) 98 | -------------------------------------------------------------------------------- /src/components/ControlPanel/LFO.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 82 | -------------------------------------------------------------------------------- /src/components/ControlPanel/SettingsExchange.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 104 | -------------------------------------------------------------------------------- /src/composables/useMidiScheduler.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { clearTimeout, setTimeout } from 'worker-timers' 3 | import sendMIDIMessage from '../assets/js/midiMessages.js' 4 | import { getMIDINote } from '../assets/js/getMIDINote.js' 5 | import { getRandomTimeGap } from '../assets/js/helpers.js' 6 | 7 | export function useMidiScheduler(settings, status, bynaryInSelectedBitness, frequencyCoefficients) { 8 | const midiTimeoutIDs = ref([]) 9 | const commands = ref([]) 10 | const commandForNoteOff = ref(null) 11 | 12 | function clearMidiTimeouts() { 13 | midiTimeoutIDs.value.forEach((id) => clearTimeout(id)) 14 | } 15 | 16 | function playNote(index) { 17 | // NoteOff is not sent in solidMode (only allSoundOff at the end of reading) 18 | // If there are identical commands in a row, noteOn and pitch are not played 19 | // The identical commands in continuous mode are compared by note and its pitch 20 | // In tempered mode only by note 21 | 22 | if (!settings.midi.solidMode) { 23 | if (index !== 0) { 24 | sendMIDIMessage.noteOff(commands.value[index - 1][0], settings.midi.velocity, settings.midi.port, settings.midi.channel) 25 | } 26 | 27 | // If we have changed the frequency/note range, then turn off the previous note 28 | if (commandForNoteOff.value) { 29 | sendMIDIMessage.noteOff(commandForNoteOff.value[0], settings.midi.velocity, settings.midi.port, settings.midi.channel) 30 | commandForNoteOff.value = null 31 | } 32 | 33 | sendMIDIMessage.noteOn(commands.value[index][0], settings.midi.velocity, settings.midi.port, settings.midi.channel) 34 | 35 | if (settings.frequencyMode === 'continuous') { 36 | sendMIDIMessage.pitch(commands.value[index][1], settings.midi.port, settings.midi.channel) 37 | } 38 | } else { 39 | let isEqualNote = null 40 | if (index !== 0) { 41 | if (settings.midi.solidMode) { 42 | if (settings.frequencyMode === 'continuous') { 43 | isEqualNote = commands.value[index - 1][0] === commands.value[index][0] && commands.value[index - 1][1] === commands.value[index][1] 44 | } else { 45 | isEqualNote = commands.value[index - 1][0] === commands.value[index][0] 46 | } 47 | } 48 | } 49 | 50 | if (!isEqualNote) { 51 | sendMIDIMessage.noteOn(commands.value[index][0], settings.midi.velocity, settings.midi.port, settings.midi.channel) 52 | 53 | if (settings.frequencyMode === 'continuous') { 54 | sendMIDIMessage.pitch(commands.value[index][1], settings.midi.port, settings.midi.channel) 55 | } 56 | } 57 | } 58 | 59 | if (settings.isRandomTimeGap && index !== 0) status.currentCommand++ 60 | } 61 | 62 | function planMidiList(startOfList, endOfList, indexOffset = 0) { 63 | clearMidiTimeouts() 64 | for (let binaryID = startOfList, index = 0; binaryID <= endOfList; binaryID++, index++) { 65 | commands.value[index] = getMIDINote( 66 | bynaryInSelectedBitness.value[binaryID], 67 | settings.bitness, 68 | settings.frequencyMode, 69 | frequencyCoefficients.value, 70 | settings.frequenciesRange.from, 71 | settings.notesRange.from 72 | ) 73 | const timeoutedNote = playNote.bind(null, index) 74 | const delay = ((index + indexOffset) * settings.readingSpeed + getRandomTimeGap(settings.isRandomTimeGap, settings.readingSpeed)) 75 | midiTimeoutIDs.value[index] = setTimeout(timeoutedNote, delay * 1000) 76 | } 77 | } 78 | 79 | function recalculateCommands(startOfList, endOfList) { 80 | for (let binaryID = startOfList, index = 0; binaryID <= endOfList; binaryID++, index++) { 81 | commands.value[index] = getMIDINote( 82 | bynaryInSelectedBitness.value[binaryID], 83 | settings.bitness, 84 | settings.frequencyMode, 85 | frequencyCoefficients.value, 86 | settings.frequenciesRange.from, 87 | settings.notesRange.from 88 | ) 89 | } 90 | } 91 | 92 | return { 93 | midiTimeoutIDs, 94 | commands, 95 | commandForNoteOff, 96 | clearMidiTimeouts, 97 | playNote, 98 | planMidiList, 99 | recalculateCommands, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/ControlPanel/ControlPanel.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 111 | 112 | 145 | -------------------------------------------------------------------------------- /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/components/ControlPanel/InteractiveInput.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 147 | 148 | 211 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Frequency.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 144 | 145 | 165 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Midi.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 181 | -------------------------------------------------------------------------------- /src/composables/useAudioGraph.js: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import fourierCoefficients from '../assets/js/fourierCoefficients.js' 3 | 4 | export function useAudioGraph(settings) { 5 | // Audio nodes 6 | const audioContext = ref(null) 7 | const oscillator = ref(null) 8 | const gain = ref(null) 9 | const filter = ref(null) 10 | const lfoDepth = ref(null) 11 | const lfoOsc = ref(null) 12 | const masterGain = ref(null) 13 | const panner = ref(null) 14 | const squareWave = ref(null) 15 | const sawtoothWave = ref(null) 16 | 17 | function createAudioGraph(sampleRate) { 18 | if (sampleRate) { 19 | audioContext.value = new AudioContext({ sampleRate }) 20 | } else { 21 | audioContext.value = new AudioContext() 22 | settings.sampleRate = audioContext.value.sampleRate 23 | } 24 | 25 | gain.value = audioContext.value.createGain() 26 | filter.value = audioContext.value.createBiquadFilter() 27 | lfoDepth.value = audioContext.value.createGain() 28 | lfoOsc.value = audioContext.value.createOscillator() 29 | masterGain.value = audioContext.value.createGain() 30 | panner.value = audioContext.value.createStereoPanner() 31 | 32 | // Setup 33 | filter.value.type = 'lowpass' 34 | filter.value.frequency.value = settings.biquadFilterFrequency 35 | filter.value.Q.value = settings.biquadFilterQ 36 | lfoDepth.value.gain.value = settings.LFO.depth 37 | gain.value.gain.value = settings.gain 38 | masterGain.value.gain.value = 1 39 | panner.value.pan.value = settings.panner 40 | 41 | // Connection 42 | filter.value.connect(gain.value).connect(masterGain.value).connect(panner.value).connect(audioContext.value.destination) 43 | lfoDepth.value.connect(masterGain.value.gain) 44 | 45 | // Set specific waves 46 | squareWave.value = audioContext.value.createPeriodicWave( 47 | Float32Array.from(fourierCoefficients.square.real), 48 | Float32Array.from(fourierCoefficients.square.imag) 49 | ) 50 | sawtoothWave.value = audioContext.value.createPeriodicWave( 51 | Float32Array.from(fourierCoefficients.sawtooth.real), 52 | Float32Array.from(fourierCoefficients.sawtooth.imag) 53 | ) 54 | } 55 | 56 | function deleteAudioGraph() { 57 | audioContext.value = null 58 | oscillator.value = null 59 | gain.value = null 60 | filter.value = null 61 | lfoDepth.value = null 62 | lfoOsc.value = null 63 | masterGain.value = null 64 | panner.value = null 65 | squareWave.value = null 66 | sawtoothWave.value = null 67 | } 68 | 69 | function setLFOType() { 70 | if (!lfoOsc.value) return 71 | 72 | if (settings.LFO.type === 'square2') { 73 | lfoOsc.value.setPeriodicWave(squareWave.value) 74 | } else if (settings.LFO.type === 'sawtooth2') { 75 | lfoOsc.value.setPeriodicWave(sawtoothWave.value) 76 | } else { 77 | lfoOsc.value.type = settings.LFO.type 78 | } 79 | } 80 | 81 | function setOscillators() { 82 | oscillator.value = audioContext.value.createOscillator() 83 | 84 | if (settings.waveType === 'square2') { 85 | oscillator.value.setPeriodicWave(squareWave.value) 86 | } else if (settings.waveType === 'sawtooth2') { 87 | oscillator.value.setPeriodicWave(sawtoothWave.value) 88 | } else { 89 | oscillator.value.type = settings.waveType 90 | } 91 | 92 | oscillator.value.connect(filter.value) 93 | 94 | lfoOsc.value = audioContext.value.createOscillator() 95 | 96 | setLFOType() 97 | 98 | lfoOsc.value.frequency.value = settings.LFO.rate 99 | lfoOsc.value.connect(lfoDepth.value) 100 | } 101 | 102 | function recreateAudioGraph(sampleRate) { 103 | deleteAudioGraph() 104 | createAudioGraph(sampleRate) 105 | setOscillators() 106 | } 107 | 108 | // Watchers for realtime parameter changes 109 | function setupAudioWatchers(status) { 110 | watch( 111 | () => settings.sampleRate, 112 | () => recreateAudioGraph(settings.sampleRate) 113 | ) 114 | 115 | watch( 116 | () => settings.biquadFilterFrequency, 117 | (newValue) => { 118 | if (!filter.value) return 119 | filter.value.frequency.value = newValue 120 | } 121 | ) 122 | 123 | watch( 124 | () => settings.biquadFilterQ, 125 | (newValue) => { 126 | if (!filter.value) return 127 | filter.value.Q.value = newValue 128 | } 129 | ) 130 | 131 | watch( 132 | () => settings.LFO.enabled, 133 | (newValue) => { 134 | if (!lfoOsc.value) return 135 | if (newValue) { 136 | if (status.playing) { 137 | lfoOsc.value.start() 138 | } 139 | } else { 140 | if (status.playing) { 141 | lfoOsc.value.stop(audioContext.value.currentTime + 0.1) 142 | lfoOsc.value = audioContext.value.createOscillator() 143 | setLFOType() 144 | lfoOsc.value.frequency.value = settings.LFO.rate 145 | lfoOsc.value.connect(lfoDepth.value) 146 | } 147 | } 148 | } 149 | ) 150 | 151 | watch( 152 | () => settings.LFO.type, 153 | () => setLFOType() 154 | ) 155 | 156 | watch( 157 | () => settings.LFO.rate, 158 | (newValue) => { 159 | if (!lfoOsc.value) return 160 | lfoOsc.value.frequency.value = newValue 161 | } 162 | ) 163 | 164 | watch( 165 | () => settings.LFO.depth, 166 | (newValue) => { 167 | if (!lfoDepth.value || !audioContext.value) return 168 | lfoDepth.value.gain.setTargetAtTime(newValue, audioContext.value.currentTime, 0.005) 169 | } 170 | ) 171 | 172 | watch( 173 | () => settings.gain, 174 | (newValue) => { 175 | if (!gain.value || !audioContext.value) return 176 | gain.value.gain.setTargetAtTime(newValue, audioContext.value.currentTime, 0.005) 177 | } 178 | ) 179 | 180 | watch( 181 | () => settings.panner, 182 | (newValue) => { 183 | if (!panner.value || !audioContext.value) return 184 | panner.value.pan.setTargetAtTime(newValue, audioContext.value.currentTime, 0.005) 185 | } 186 | ) 187 | 188 | watch( 189 | () => settings.waveType, 190 | (newValue) => { 191 | if (!oscillator.value) return 192 | if (newValue === 'square2') { 193 | oscillator.value.setPeriodicWave(squareWave.value) 194 | } else if (newValue === 'sawtooth2') { 195 | oscillator.value.setPeriodicWave(sawtoothWave.value) 196 | } else { 197 | oscillator.value.type = newValue 198 | } 199 | } 200 | ) 201 | } 202 | 203 | return { 204 | audioContext, 205 | oscillator, 206 | gain, 207 | filter, 208 | lfoDepth, 209 | lfoOsc, 210 | masterGain, 211 | panner, 212 | squareWave, 213 | sawtoothWave, 214 | createAudioGraph, 215 | deleteAudioGraph, 216 | setLFOType, 217 | setOscillators, 218 | recreateAudioGraph, 219 | setupAudioWatchers, 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/components/FileInput.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 149 | 150 | 250 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Binary Synth 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/ControlPanel/Global.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 200 | -------------------------------------------------------------------------------- /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?si=D9GdKJF3hmjvw4fg&t=654 10 | 11 | **Performance and lecture**: https://youtu.be/Vj476GVtHZU?si=y4QY3JSnzjmhn5ur 12 | 13 | **Article**: https://bs.stranno.su/drone-ambient-noise-synthesizer ([Dev.to](https://dev.to/max_alyokhin/drone-ambient-noise-synthesizer-in-javascript-when-instability-is-a-feature-not-a-bug-34i8) | [Habr](https://habr.com/ru/articles/970404)) 14 | 15 | ![](https://store.stranno.su/bs/fuji2.png) 16 | 17 | _Эта страница есть также на русском_ 18 | 19 | A web-synthesizer that generates sound from the binary code of any files (databending-instrument). 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 450kb. The synthesizer doesn't need internet, it can be downloaded and run locally on any device with a browser. 20 | 21 | 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. 22 | 23 | ## Quick start guide 24 | 25 | - open one of these links: [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}}) | [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}}) | [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}}) 26 | - tap anywhere on the screen and upload any file (files are not sent anywhere, everything is computed on your device) 27 | - press the spacebar or the Play button 28 | - press and hold the `S` key and move the mouse. You can move the mouse indefinitely. Pressing `Esc` will return the cursor 29 | - with the `S` key held down, press `Shift` to “strengthen” or `Ctrl` to “weaken” the input 30 | - do similar actions with other settings by pressing the keys `Q`, `W`, `A`, `Z`, `X`, `C`, `V`, etc. (input fields have the corresponding key on the right side) 31 | - open several more tabs with the instrument in parallel and launch 32 | - move between tabs using `Ctrl + tab number` (9 max) or `Ctrl + Tab` / `Ctrl + Shift + Tab` 33 | - process the sound on-the-fly by connecting effects pedals, or by using Ableton or other DAWs and [Virtual audio cable](https://vac.muzychenko.net/en/) 34 | - switch to MIDI mode and control a virtual or external synthesizer using the [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html) 35 | - save settings just by copying the URL or via Save settings button 36 | - copy the instrument to play without internet by right-clicking - Save As, or download from here from the folder `/dist` 37 | 38 | ## Contents 39 | 40 | - [Application principle](#principle) 41 | - [Switching to granular mode](#granular) 42 | - [Recommendations for optimal performance](#recommendation) 43 | - [MIDI](#midi) 44 | - [Interface features](#interface) 45 | - [Saving settings](#settings) 46 | - [Run locally and build the project](#run) 47 | 48 | ## Application principle 49 | 50 | 51 | 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. 52 | 53 | But what if the file format and the interpreter don't match? 54 | 55 | In the case of musical experimentation, there have been earlier attempts, for example, to "play" a file through an audio editor. 56 | 57 | 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. 58 | 59 | 1. We can interpret files as an array of numbers. That is, we divide continuous machine code into _words_ of some information capacity (bitness): 60 | 61 | - 8 bits (numbers from 0 to 255) 62 | - 16 bits (numbers from 0 to 65 535) 63 | 64 | 2. Then, each word is a command that defines the frequency of the sound 65 | 66 | 3. At the level of the whole system, we set global parameters: 67 | 68 | - speed of interpretation 69 | - musical scale (or lack thereof), range of notes/frequencies 70 | - looping 71 | - MIDI mode 72 | - smooth or abrupt transition between commands 73 | - settings of virtual devices required for synthesis (oscillator, filter, LFO) or MIDI settings 74 | 75 | 4. To reduce the load on the device, we divide the file into chunks of 500 commands each 76 | 77 | 5. Recursively schedule the synthesis control by reading 500 instructions per iteration and using global parameters 78 | 79 | 6. If we have reached the end of the file, stop execution or start again 80 | 81 | ## Switching to granular mode 82 | 83 | 84 | > **Note**: Here and below the instrument interface terms are used. For their description, see below in the Interface features section 85 | 86 | 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. 87 | 88 | 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. 89 | 90 | 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. 91 | 92 | *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.* 93 | 94 | 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". 95 | 96 | ![](https://store.stranno.su/bs/granular.jpg) 97 | 98 | ## Recommendations for optimal performance 99 | 100 | 101 | - Use incognito mode with extensions disabled 102 | - Close the non-incognito browser 103 | - Leave only BS tabs in incognito mode 104 | - Use a separate browser [ungoogled-chromium](https://github.com/ungoogled-software/ungoogled-chromium), which uses a little less CPU and a lot less RAM. 105 | 106 | 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. 107 | 108 | 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. 109 | 110 | ## MIDI 111 | 112 | 113 | 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. 114 | 115 | MIDI messages can be sent: 116 | 117 | - 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)) 118 | - in DAW and other applications with virtual synthesizers (i.e. BS can control, for example, synthesizer in Ableton). 119 | - to MIDI-compatible external devices connected to a computer 120 | 121 | To send MIDI messages to a DAW on Windows devices, you can use [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). 122 | 123 | > **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 124 | 125 | > **Note**: MIDI messages are generated on the desktop only 126 | 127 | ## Interface features 128 | 129 | 130 | - `Reading speed` — interpretation speed; at high speeds over 0.001 the application may become unstable 131 | 132 | - `Bitness` — we can divide the binary code into words of 8 or 16 bits, which changes the number of available frequencies (256 or 65536) 133 | 134 | - `Panner` — pan between left (-1) and right (1) channels 135 | 136 | - `Frequency generation mode` 137 | 138 | - `continuous` — continuous frequency distribution 139 | - `tempered` — distribution by 12-step equal-tempered scale. There are notes from C-2 to B8 140 | 141 | - `Transition type` — transition between frequencies 142 | 143 | - `immediate` — instantaneous, rough transition 144 | - `linear` — linearly to the next frequency 145 | - `exponential` — exponentially to the next frequency 146 | 147 | - `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 148 | 149 | - `Fragment` — allows to play not the whole file, but a certain part of it 150 | 151 | - `Sample rate` — sampling frequency. In most cases it has little effect on the sound, but it is very important when playing at ultra-low frequencies like frequencies range 0 - 1 152 | 153 | - `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 154 | 155 | - `Last noteOn mode` — leaves the last command in the loop pressed. Allows to make smoother transitions between repeats of patterns 156 | 157 | - 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` 158 | 159 | ## Saving settings 160 | 161 | 162 | You can save your settings in two ways: 163 | 164 | - 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. 165 | - 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. 166 | 167 | ## Run locally and build the project 168 | 169 | 170 | ### Just copy the app 171 | 172 | 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. 173 | 174 | ### Build locally to work with the code 175 | 176 | Tech stack: Vue3 + Pinia + Vite. 177 | 178 | 1. Download and install the LTS version of Node.js 179 | 2. Download the code directly from Github, or via `git clone`. 180 | 3. In the project folder in the terminal execute: 181 | 182 | ```bash 183 | npm i 184 | npm run dev # development-build 185 | npm run build # production-build, generate index.html with everything we need 186 | ``` 187 | 188 | For MIDI tests, you can use this resource https://studiocode.dev/midi-monitor/ 189 | -------------------------------------------------------------------------------- /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?si=D9GdKJF3hmjvw4fg&t=654 10 | 11 | **Концерт и лекция**: https://youtu.be/Vj476GVtHZU?si=y4QY3JSnzjmhn5ur 12 | 13 | **Статья**: https://bs.stranno.su/drone-ambient-noise-synthesizer ([Dev.to](https://dev.to/max_alyokhin/drone-ambient-noise-synthesizer-in-javascript-when-instability-is-a-feature-not-a-bug-34i8) | [Habr](https://habr.com/ru/articles/970404)) 14 | 15 | ![](https://store.stranno.su/bs/fuji2.png) 16 | 17 | Веб-синтезатор, генерирующий звук из двоичного кода любых файлов (databending-инструмент). Позволяет синтезировать звук прямо в браузере, либо быть генератором MIDI-сообщений во внешние устройства или DAW, превращая любой файл в партитуру. Весь код приложения написан на Javascript и вместе со всем необходимым упаковывается в один `.html`-файл размером около 450kb. Инструменту не нужен интернет, его можно скачать и запускать локально на любом устройстве где есть браузер. 18 | 19 | Приложение последовательно читает файл и за счёт высокой скорости чтения и случайной величины отклонения длительности чтения, можно получить достаточно непредсказуемую генерацию тембров, а при определённых настройках перейти в гранулярный синтез. 20 | 21 | ## Быстрый старт и краткое руководство 22 | 23 | - откройте одну из этих ссылок: [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}}) | [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}}) | [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 | - нажмите пробел или кнопку Play 26 | - нажмите и удерживайте клавишу `S` и подвигайте мышью. Мышью можно двигать бесконечно. Нажатие `Esc` вернёт курсор 27 | - при зажатой клавише `S` нажмите `Shift` для "усиления" или `Ctrl` для "ослабления" ввода 28 | - проделайте похожие действия с другими настройками через зажатие клавиш `Q`, `W`, `A`, `Z`, `X`, `C`, `V` и т.д. (у полей ввода справа указана соответствующая клавиша) 29 | - откройте параллельно ещё несколько вкладок с инструментом и запустите 30 | - перемещайтесь между вкладками через `Ctrl + номер вкладки` (9 максимум), либо `Ctrl + Tab` / `Ctrl + Shift + Tab` 31 | - обработайте звук налету, подключив педали эффектов, либо с помощью Ableton или других DAW и [Virtual audio cable](https://vac.muzychenko.net/en/) 32 | - переключитесь в MIDI-режим и управляйте виртуальным или внешним синтезатором с помощью [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html) 33 | - сохраните настройки просто скопировав URL или через Save settings 34 | - скопируйте инструмент, чтобы играть без интернета, нажав на правую кнопку мыши — Сохранить как, либо скачайте отсюда из папки `/dist` 35 | 36 | ## Содержание 37 | 38 | - [Принцип работы](#principle) 39 | - [Переход в гранулярный режим](#granular) 40 | - [Рекомендации по оптимальной работе](#recommendation) 41 | - [MIDI](#midi) 42 | - [Интерфейс](#interface) 43 | - [Сохранение настроек](#settings) 44 | - [Запуск локально и сборка проекта](#run) 45 | 46 | ## Принцип работы 47 | 48 | 49 | Все данные на любом компьютере или смартфоне представлены в виде файлов (являющихся, по своей сути, текстами). Содержанием этих файлов в конечном итоге являются просто нули и единицы. И эти нули и единицы, в общем-то, все одинаковые, поэтому нам нужен интерпретатор, для того чтобы извлечь смысл из этих текстов. Можно сказать, что формат файла (.mp3, .docx и т.д.) это просто указатель, какому интерпретатору надо передать текст, чтобы из него извлечь смысл. 50 | 51 | Но что, если формат файла и интерпретатор не совпадают? 52 | 53 | Что касается музыкальных экспериментов, то ранее были, например, попытки "воспроизвести" текстовый или иной файл через аудио-редактор. 54 | 55 | Мы могли бы пойти дальше и написать собственный интерпретатор, который смотрел бы на файлы безотносительно формата, использовал собственную "манеру чтения" исходных нулей и единиц и на этой основе предоставлял полноценную систему управляемого синтеза звуков. 56 | 57 | 1. Мы можем интерпретировать файлы как массив чисел. То есть, мы разбиваем непрерывный машинный код на _слова_ некоторой информационной ёмкости (разрядности): 58 | 59 | - 8 бит (числа от 0 до 255) 60 | - 16 бит (числа от 0 до 65 535) 61 | 62 | 2. Тогда, каждое слово есть команда, определяющая частоту звука 63 | 64 | 3. На уровне всей системы мы задаём глобальные параметры: 65 | 66 | - скорость интерпретации 67 | - наличие случайной величины разброса скорости интерпретации 68 | - музыкальный строй (или его отсутствие), диапазон нот/частот; по этому диапазону равномерно сопоставляются частоты по 256 или 65 536 возможным комбинациям нулей и единиц 69 | - зацикленность воспроизведения 70 | - режим MIDI 71 | - плавный или резкий переход между командами 72 | - настройки виртуальных устройств, необходимых для синтеза (осциллятор, фильтр, LFO), либо настройки MIDI 73 | 74 | 4. Чтобы снизить нагрузку на устройство, делим файл на куски по 500 команд 75 | 76 | 5. Рекурсивно планируем управление синтезом, читая по 500 команд в итерации и используя глобальные параметры 77 | 78 | 6. Если дошли до конца файла, прекращаем исполнение, либо начинаем заново 79 | 80 | ## Переход в гранулярный режим 81 | 82 | 83 | > **Note**: Здесь и далее используются термины интерфейса инструмента. Их описание смотрите ниже в разделе Особенности интерфейса 84 | 85 | Гранулярный синтез оперирует мелкими кусочками звуков — акустическими пикселями. Принято считать, что гранулярный синтез "начинается" при оперировании звуками <50мс. При значениях `fragment` * `reading speed` = <50 мы начинаем оперировать уже акустическими пикселями. 86 | 87 | При этом каждую команду из `fragment` можно считать "субпикселем", который, при включённом `random time gap`, каждый раз уникален, а пиксель, соответственно, уникален кратно количеству субпикселей. В итоге мы получаем мутирующий тембр. 88 | 89 | В классическом гранулярном синтезе пиксели играют одновременно и параллельно, и их количество может меняться со временем. В BS же пиксели образуют нить, по которой мы движемся. 90 | 91 | *То есть, если в обычном гранулярном синтезе на слушателя как бы опрокидывают грузовик с песком, где каждая песчинка это акустический пиксель, то здесь этот песок высыпается через воронку диаметром с одну песчинку и вот эту тонкую струю мы и наблюдаем.* 92 | 93 | На изображении ниже показано формирование акустического пикселя из двух команд (`fragment: from = 0, to = 1`), при скорости чтения 0.005 с. Необходимо учитывать, что у каждой частоты есть период *T*, равный отношению единицы времени (1 секунды = 1000 миллисекунд) к частоте. Это значит, что мы можем мыслить звук не только через частоту, но и через время, за которое волна делает одно полное колебание. Если волна не успевает сделать полное колебание, такой объект называется "вейвлетом". 94 | 95 | ![](https://store.stranno.su/bs/granular.jpg) 96 | 97 | ## Рекомендации по оптимальной работе 98 | 99 | 100 | - Использовать режим инкогнито при отключённых расширениях 101 | - Браузер не-инкогнито закрыть 102 | - В инкогнито оставить только вкладки с BS 103 | - Использовать отдельный браузер [ungoogled-chromium](https://github.com/ungoogled-software/ungoogled-chromium), потребляющий немного меньше CPU и сильно меньше ОЗУ. 104 | 105 | BS под нагрузкой требует в среднем до 7.1% CPU, в режиме инкогнито 6%, Firefox 4.2%, но работает менее стабильно. Также открытая консоль/DevTools браузера повышает потребление CPU на каждой вкладке на 10%. Рекомендуется использовать BS в режиме инкогнито без любых других открытых вкладок, кроме вкладок BS, для максимальной эффективности. 106 | 107 | Более интересный звук получается при нескольких независимых экземплярах BS в разных вкладках. Теоретически, можно было бы реализовать несколько потоков работы BS в одной вкладке, но это менее оптимально, так как браузеры ограничивают максимальное потребление CPU на вкладку (в Chrome это 10% CPU). Также, у каждой вкладки свой [event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop). Для быстрого переключения между вкладками можно использовать `ctrl + номер вкладки`. 108 | 109 | ## MIDI 110 | 111 | 112 | При включении MIDI-режима автоматически выбирается первый попавшийся порт из доступных и его первый канал. Далее последовательно при чтении посылается сигнал `noteOn`, через время `reading speed` посылается сигнал `noteOff`. В `continuous` режиме после каждого `noteOn` посылается `Pitch` сигнал, чтобы попасть в нужную частоту. 113 | 114 | MIDI-сообщения могут посылаться: 115 | 116 | - в соседние вкладки и окна браузеров, если они слушают MIDI (например, в веб-аналог [DX7](http://mmontag.github.io/dx7-synth-js)) 117 | - в DAW и прочие приложения, где есть виртуальные синтезаторы (то есть BS может управлять, например, синтезатором в Ableton) 118 | - во внешние устройства, поддерживающие MIDI и подключённые к компьютеру 119 | 120 | Для передачи MIDI-сообщений в DAW на устройствах Windows можно воспользоваться [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html). 121 | 122 | > **Note**: После любых манипуляций с MIDI-портами (подключение/отключение/переподключение) необходимо полностью перезапустить браузер, закрыв все окна браузера если их несколько 123 | 124 | > **Note**: MIDI-сообщения генерируются только на десктопе 125 | 126 | ## Интерфейс 127 | 128 | 129 | - `Reading speed` — скорость интерпретации; на высоких скоростях более 0.001 приложение может работать нестабильно 130 | 131 | - `Bitness` — мы можем разделить двоичный код на слова по 8 или 16 бит, что меняет количество доступных частот (256 или 65536) 132 | 133 | - `Panner` — панорамирование между левым (-1) и правым (1) каналами 134 | 135 | - `Frequency generation mode` 136 | 137 | - `continuous` — непрерывное распределение частот 138 | - `tempered` — распределение по 12-ступенному равномерно-темперированному строю. Доступны ноты от C-2 до B8 139 | 140 | - `Transition type` — переход между частотами 141 | 142 | - `immediately` — моментально, грубый переход 143 | - `linear` — линейно до следующей частоты 144 | - `exponential` — экспоненциально до следующей частоты 145 | 146 | - `Random time gap` — добавление случайной величины времени до следующего звука в пределах параметра `reading speed`. Делает звук менее "роботизированным", так как расстояние до каждого звука немного отличается и это добавляет больше "живости" игре 147 | 148 | - `Fragment` — позволяет играть не весь файл, а его определённую часть 149 | 150 | - `Sample rate` — частота дискретизации. В большинстве случаев слабо влияет на звук, но очень важен при игре на сверхнизких частотах вроде frequencies range 0 - 1 151 | 152 | - `Solid Mode` — режим "сплошного нажатия", не посылает команды `noteOff`; если подряд идут одинаковые команды (и как следствие ноты), то даже noteOn не посылается. В конце посылается `allSoundOff`. На некоторых синтезаторах позволять осуществить плавные переходы между нотами 153 | 154 | - `Last noteOn mode` — оставляет нажатой последнюю команду в лупе. Позволяет делать более плавные переходы между повторами паттернов. 155 | 156 | - У некоторых полей ввода есть клавиатурное сокращение: при нажатии соответствующей клавиши автоматически наводится фокус на элемент. При зажатии клавиши и одновременном движении мышью можно плавно менять значения. При нажатии Shift можно увеличить (10x, 100x, 1000x) "мощность" изменения значения, при нажатии на Ctrl уменьшить (0.1x, 0.01x, 0.001x). Курсор при этом пропадает, чтобы иметь возможность бесконечно изменять значения. Для возврата курсора необходимо нажать Esc. 157 | 158 | ## Сохранение настроек 159 | 160 | 161 | Сохранить настройки можно двумя путями: 162 | 163 | - через URL-ссылку. При щелчке в адресную строку браузера приложение автоматически формирует ссылку, в которую записываются настройки. Вы можете скопировать её и при открытии ссылки настройки сразу применятся, вам остаётся только загрузить файл для синтеза звука. 164 | - через файл. В интерфейсе преусмотрены кнопки Save / Load / Restore settings, которые позволяют сохранить на компьютер или загрузить из него файл с настройками, а также восстановить исходные. 165 | 166 | ## Запуск локально и сборка проекта 167 | 168 | 169 | ### Просто скопировать приложение 170 | 171 | Всё необходимое для работы системы заложено в единственный `.html` файл, который можно скачать в папке `dist`, либо просто перейти на https://bs.strannо.su и, нажав правую кнопку мыши, в меню выбрать Сохранить как. 172 | 173 | ### Собрать билд локально для доработки кода 174 | 175 | Tech stack: Vue3 + Pinia + Vite. 176 | 177 | 1. Скачать и установить LTS версию Node.js 178 | 2. Скачать код напрямую с Github, либо через `git clone` 179 | 3. В папке с проектом в терминале выполнить: 180 | 181 | ```bash 182 | npm i 183 | npm run dev # development-сборка 184 | npm run build # production-сборка, генерирует index.html со всем необходимым 185 | ``` 186 | 187 | Для тестов MIDI можно пользоваться этим ресурсом https://studiocode.dev/midi-monitor/ -------------------------------------------------------------------------------- /src/components/Status.vue: -------------------------------------------------------------------------------- 1 | 175 | 176 | 220 | 221 | 312 | -------------------------------------------------------------------------------- /src/composables/usePlaybackControl.js: -------------------------------------------------------------------------------- 1 | import { ref, computed, watch } from 'vue' 2 | import { clearTimeout, setTimeout } from 'worker-timers' 3 | import { toFixedNumber } from '../assets/js/helpers.js' 4 | import sendMIDIMessage from '../assets/js/midiMessages.js' 5 | 6 | export function usePlaybackControl(file, settings, status, audioGraph, oscillatorScheduler, midiScheduler) { 7 | const nextListTimeoutID = ref(null) 8 | 9 | const listSize = computed(() => (settings.bitness === '8' ? 500 : 250)) 10 | const isOnlyOneList = computed(() => listSize.value >= settings.fragment.to - settings.fragment.from) 11 | const timeToNextList = computed(() => 12 | toFixedNumber((status.startAndEndOfList[1] - status.startAndEndOfList[0] + 1) * settings.readingSpeed * 1000) 13 | ) 14 | 15 | function endOfListFor(startOfList) { 16 | return startOfList + listSize.value <= settings.fragment.to ? startOfList + listSize.value - 1 : settings.fragment.to 17 | } 18 | 19 | function nextList(listID, startOfList) { 20 | // If we pressed stop, we exit the recursion 21 | if (!status.playing) { 22 | stop() 23 | return 24 | } 25 | 26 | // If we have reached the end of the fragment 27 | if (startOfList >= settings.fragment.to) { 28 | // In midi mode, we turn off the last note 29 | if (settings.midiMode) { 30 | sendMIDIMessage.noteOff( 31 | midiScheduler.commands.value[status.startAndEndOfList[1] - status.startAndEndOfList[0]][0], 32 | settings.midi.velocity, 33 | settings.midi.port, 34 | settings.midi.channel 35 | ) 36 | } 37 | 38 | if (settings.loop) { 39 | nextList(0, settings.fragment.from) 40 | return 41 | } else { 42 | stop() 43 | return 44 | } 45 | } 46 | 47 | // Define block of commands 48 | const endOfList = endOfListFor(startOfList) 49 | 50 | status.startAndEndOfList = [startOfList, endOfList] 51 | status.listID = listID 52 | status.currentIteration++ 53 | 54 | // Planning the list and calculate commands 55 | if (!settings.midiMode) { 56 | oscillatorScheduler.planOscillatorList(startOfList, endOfList) 57 | } else { 58 | midiScheduler.planMidiList(startOfList, endOfList) 59 | } 60 | 61 | // We are planning the next list 62 | // If there is only one list 63 | if (isOnlyOneList.value) { 64 | if (!settings.loop) { 65 | nextListTimeoutID.value = setTimeout(() => { 66 | if (settings.midiMode) { 67 | sendMIDIMessage.noteOff( 68 | midiScheduler.commands.value[settings.fragment.to - settings.fragment.from][0], 69 | settings.midi.velocity, 70 | settings.midi.port, 71 | settings.midi.channel 72 | ) 73 | } 74 | stop() 75 | }, (settings.fragment.to - settings.fragment.from + 1) * settings.readingSpeed * 1000) 76 | } else { 77 | // In the looped mode on a single list 78 | nextListTimeoutID.value = setTimeout(() => { 79 | if (settings.midiMode && !settings.midi.solidMode && !settings.midi.lastNoteOn) { 80 | sendMIDIMessage.allSoundOff(settings.midi.port, settings.midi.channel) 81 | } 82 | nextList(0, settings.fragment.from) 83 | }, (settings.fragment.to - settings.fragment.from + 1) * settings.readingSpeed * 1000) 84 | } 85 | } else { 86 | // If there are several lists 87 | nextListTimeoutID.value = setTimeout(() => { 88 | if (settings.midiMode) { 89 | sendMIDIMessage.noteOff( 90 | midiScheduler.commands.value[midiScheduler.commands.value.length - 1][0], 91 | settings.midi.velocity, 92 | settings.midi.port, 93 | settings.midi.channel 94 | ) 95 | } 96 | nextList(++listID, (startOfList += listSize.value)) 97 | }, timeToNextList.value) 98 | } 99 | } 100 | 101 | function play() { 102 | if (file.loaded && !status.playing) { 103 | if (!settings.midiMode) { 104 | audioGraph.gain.value.gain.setTargetAtTime(settings.gain, audioGraph.audioContext.value.currentTime, 0.005) 105 | audioGraph.oscillator.value.start() 106 | if (settings.LFO.enabled) { 107 | audioGraph.lfoOsc.value.start() 108 | } 109 | } 110 | 111 | status.playing = true 112 | nextList(0, settings.fragment.from) 113 | } 114 | } 115 | 116 | function stop() { 117 | if (status.playing) { 118 | clearTimeout(nextListTimeoutID.value) 119 | status.currentIteration = 0 120 | 121 | if (!settings.midiMode) { 122 | audioGraph.gain.value.gain.setTargetAtTime(0.0001, audioGraph.audioContext.value.currentTime, 0.005) 123 | audioGraph.oscillator.value.stop(audioGraph.audioContext.value.currentTime + 0.1) 124 | if (settings.LFO.enabled) audioGraph.lfoOsc.value.stop(audioGraph.audioContext.value.currentTime + 0.1) 125 | audioGraph.oscillator.value.frequency.cancelScheduledValues(audioGraph.audioContext.value.currentTime + 0.1) 126 | 127 | audioGraph.setOscillators() 128 | } else { 129 | midiScheduler.clearMidiTimeouts() 130 | sendMIDIMessage.allSoundOff(settings.midi.port, settings.midi.channel) 131 | } 132 | 133 | status.playing = false 134 | } 135 | } 136 | 137 | function setupPlaybackWatchers(bynaryInSelectedBitness) { 138 | // If these parameters are changed, completely recalculate the scheduling again 139 | watch([() => settings.readingSpeed, () => settings.transitionType, () => settings.isRandomTimeGap], () => { 140 | if (status.playing) { 141 | clearTimeout(nextListTimeoutID.value) 142 | 143 | if (!settings.midiMode) { 144 | audioGraph.oscillator.value.frequency.cancelScheduledValues(audioGraph.audioContext.value.currentTime) 145 | 146 | const start = status.startAndEndOfList[0] + status.currentCommand 147 | const end = status.startAndEndOfList[1] 148 | // Play the next command immediately to avoid a pause on speed change 149 | if (start <= end) { 150 | const firstID = Math.min(start + 1, end) 151 | for (let binaryID = firstID, index = 0; binaryID <= end; binaryID++, index++) { 152 | const command = oscillatorScheduler.computeFrequency(bynaryInSelectedBitness.value[binaryID]) 153 | const gap = settings.isRandomTimeGap && index !== 0 ? oscillatorScheduler.getRandomTimeGap() : 0 154 | const time = audioGraph.audioContext.value.currentTime + index * settings.readingSpeed + gap 155 | oscillatorScheduler.scheduleOscillatorValue(command, time) 156 | } 157 | } 158 | } else { 159 | midiScheduler.clearMidiTimeouts() 160 | 161 | // Don't silence everything; instead, remember the current note to turn off right before the next note 162 | if (!midiScheduler.commandForNoteOff.value) { 163 | midiScheduler.commandForNoteOff.value = midiScheduler.commands.value[status.currentCommand] 164 | } 165 | 166 | const start = status.startAndEndOfList[0] + status.currentCommand 167 | const end = status.startAndEndOfList[1] 168 | midiScheduler.planMidiList(start, end) 169 | // Trigger the next note immediately to avoid a pause equal to readingSpeed 170 | const immediateIndex = end - start >= 1 ? 1 : 0 171 | if (midiScheduler.midiTimeoutIDs.value[immediateIndex]) { 172 | clearTimeout(midiScheduler.midiTimeoutIDs.value[immediateIndex]) 173 | } 174 | midiScheduler.playNote(immediateIndex) 175 | } 176 | 177 | // Reschedule the recursion 178 | if (listSize.value >= settings.fragment.to - settings.fragment.from) { 179 | if (!settings.loop) { 180 | nextListTimeoutID.value = setTimeout(() => { 181 | if (settings.midiMode) { 182 | sendMIDIMessage.noteOff( 183 | midiScheduler.commands.value[settings.fragment.to - settings.fragment.from][0], 184 | settings.midi.velocity, 185 | settings.midi.port, 186 | settings.midi.channel 187 | ) 188 | } 189 | stop() 190 | }, (settings.fragment.to - settings.fragment.from - status.currentCommand) * settings.readingSpeed * 1000) 191 | } else { 192 | nextListTimeoutID.value = setTimeout(() => { 193 | if (settings.midiMode) { 194 | sendMIDIMessage.noteOff( 195 | midiScheduler.commands.value[settings.fragment.to - settings.fragment.from][0], 196 | settings.midi.velocity, 197 | settings.midi.port, 198 | settings.midi.channel 199 | ) 200 | } 201 | nextList(0, settings.fragment.from) 202 | }, (settings.fragment.to - settings.fragment.from - status.currentCommand) * settings.readingSpeed * 1000) 203 | } 204 | } else { 205 | let listID = status.listID 206 | let startOfList = status.startAndEndOfList[0] 207 | 208 | nextListTimeoutID.value = setTimeout(() => { 209 | if (settings.midiMode) { 210 | sendMIDIMessage.noteOff( 211 | midiScheduler.commands.value[midiScheduler.commands.value.length - 1][0], 212 | settings.midi.velocity, 213 | settings.midi.port, 214 | settings.midi.channel 215 | ) 216 | } 217 | 218 | nextList(++listID, (startOfList += listSize.value)) 219 | }, (status.startAndEndOfList[1] - (status.startAndEndOfList[0] + status.currentCommand)) * settings.readingSpeed * 1000) 220 | } 221 | } 222 | }) 223 | 224 | // When changing these parameters, only the frequencies are recalculated 225 | watch( 226 | [() => settings.frequenciesRange, () => settings.notesRange, () => settings.frequencyMode], 227 | () => { 228 | if (status.playing) { 229 | if (!settings.midiMode) { 230 | audioGraph.oscillator.value.frequency.cancelScheduledValues(audioGraph.audioContext.value.currentTime) 231 | 232 | const start = status.startAndEndOfList[0] + status.currentCommand 233 | const end = status.startAndEndOfList[1] 234 | for (let binaryID = start, index = 0; binaryID <= end; binaryID++, index++) { 235 | const command = oscillatorScheduler.computeFrequency(bynaryInSelectedBitness.value[binaryID]) 236 | const time = audioGraph.audioContext.value.currentTime + index * settings.readingSpeed 237 | oscillatorScheduler.scheduleOscillatorValue(command, time) 238 | } 239 | } else { 240 | if (!midiScheduler.commandForNoteOff.value) { 241 | midiScheduler.commandForNoteOff.value = midiScheduler.commands.value[status.currentCommand] 242 | } 243 | 244 | midiScheduler.recalculateCommands(status.startAndEndOfList[0], status.startAndEndOfList[1]) 245 | } 246 | } 247 | }, 248 | { deep: true } 249 | ) 250 | 251 | // If bitness changed, recalc current list bounds 252 | watch( 253 | () => settings.bitness, 254 | () => { 255 | const endOfList = 256 | settings.fragment.from + listSize.value < settings.fragment.to 257 | ? settings.fragment.from + listSize.value - 1 258 | : settings.fragment.to 259 | 260 | status.startAndEndOfList = [settings.fragment.from, endOfList] 261 | } 262 | ) 263 | 264 | watch( 265 | [() => settings.fragment.from, () => settings.fragment.to], 266 | () => { 267 | const endOfList = 268 | settings.fragment.from + listSize.value < settings.fragment.to 269 | ? settings.fragment.from + listSize.value - 1 270 | : settings.fragment.to 271 | 272 | status.startAndEndOfList = [settings.fragment.from, endOfList] 273 | } 274 | ) 275 | 276 | watch( 277 | () => file.loaded, 278 | () => stop() 279 | ) 280 | 281 | watch( 282 | () => settings.midiMode, 283 | (newValue) => { 284 | if (!newValue) { 285 | audioGraph.setOscillators() 286 | } 287 | 288 | if (status.playing) { 289 | if (newValue === true) { 290 | audioGraph.oscillator.value.stop(audioGraph.audioContext.value.currentTime) 291 | if (settings.LFO.enabled) audioGraph.lfoOsc.value.stop(audioGraph.audioContext.value.currentTime) 292 | audioGraph.oscillator.value.frequency.cancelScheduledValues(audioGraph.audioContext.value.currentTime) 293 | clearTimeout(nextListTimeoutID.value) 294 | 295 | const start = status.startAndEndOfList[0] + status.currentCommand 296 | const end = status.startAndEndOfList[1] 297 | midiScheduler.planMidiList(start, end) 298 | } else { 299 | midiScheduler.clearMidiTimeouts() 300 | sendMIDIMessage.allSoundOff(settings.midi.port, settings.midi.channel) 301 | 302 | clearTimeout(nextListTimeoutID.value) 303 | 304 | audioGraph.oscillator.value.start() 305 | if (settings.LFO.enabled) audioGraph.lfoOsc.value.start() 306 | 307 | const start = status.startAndEndOfList[0] + status.currentCommand 308 | const end = status.startAndEndOfList[1] 309 | for (let binaryID = start, index = 0; binaryID <= end; binaryID++, index++) { 310 | const command = oscillatorScheduler.computeFrequency(bynaryInSelectedBitness.value[binaryID]) 311 | const t = audioGraph.audioContext.value.currentTime + index * settings.readingSpeed 312 | oscillatorScheduler.scheduleOscillatorValue(command, t) 313 | } 314 | } 315 | 316 | // Reschedule the recursion 317 | if (listSize.value >= settings.fragment.to) { 318 | if (!settings.loop) { 319 | nextListTimeoutID.value = setTimeout(() => { 320 | stop() 321 | }, (settings.fragment.to - status.currentCommand) * settings.readingSpeed * 1000) 322 | } else { 323 | nextListTimeoutID.value = setTimeout(() => { 324 | nextList(0, settings.fragment.from) 325 | }, (settings.fragment.to - status.currentCommand) * settings.readingSpeed * 1000) 326 | } 327 | } else { 328 | let listID = status.listID 329 | let startOfList = status.startAndEndOfList[0] 330 | 331 | nextListTimeoutID.value = setTimeout(() => { 332 | nextList(++listID, (startOfList += listSize.value)) 333 | }, (status.startAndEndOfList[1] - (status.startAndEndOfList[0] + status.currentCommand)) * settings.readingSpeed * 1000) 334 | } 335 | } 336 | } 337 | ) 338 | 339 | watch( 340 | () => status.playing, 341 | (newValue) => { 342 | if (!newValue) stop() 343 | } 344 | ) 345 | } 346 | 347 | return { 348 | listSize, 349 | timeToNextList, 350 | nextList, 351 | play, 352 | stop, 353 | setupPlaybackWatchers, 354 | } 355 | } 356 | --------------------------------------------------------------------------------