Skip to content

Commit

Permalink
add MIDI-regime
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxAlyokhin committed Jun 7, 2023
1 parent 2d0261a commit f8b1004
Show file tree
Hide file tree
Showing 15 changed files with 767 additions and 73 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ You can also load presets into the system (you need to download as a file and us
- [Errors](#errors)
- [Fullscreen-mode](#fullscreen)
- [Audio generation](#audio-generate)
- [MIDI](#midi)
- [Storing the interface state (settings) (Import / Export)](#state-storing)
- [Tech guide](#tech)
- [Secure context](#secure)
Expand Down Expand Up @@ -467,6 +468,19 @@ The field **Maximum value** shows the maximum speed of movement for the whole se

**Audio generation** — indicator showing whether sound is currently being generated.

### MIDI <a name="midi"></a>

AMI allows you to generate MIDI messages. To enable MIDI, the MIDI regime must be enabled. Automatically selects the first available port and its first channel. When the cutoff is exceeded, a noteOn signal is sent, then in Peak mode the noteOff signal is sent after the time specified in the Duration field; in Full mode, noteOff is sent only when the movement is over + after the time in the Duration field; during the movement, the pressure on the channel changes when speed affects the volume (that is, speed during the movement is equal to the change in the force of the key press). In Peak mode, speed increases Velocity. The MIDI Reset button sends an allSoundOff signal.

MIDI messages can be sent:
- to neighboring tabs and browser windows if they listen to MIDI (e.g., Web analog [DX7](http://mmontag.github.io/dx7-synth-js))
- to DAWs and other applications that have virtual synthesizers (that is, AMI can control, for example, a synthesizer in Ableton)
- to external MIDI-enabled devices connected to your computer

> **Note**: After any manipulations with MIDI ports (connecting/disconnecting/ reconnecting) you must restart the browser completely, closing all browser windows if there are several
> **Note**: MIDI messages are generated only on the desktop. The smartphone in this mode only sends movement and orientation data
### Saving the state (settings) of the interface <a name="state-storing"></a>

At the first start of the application, the default settings are set.
Expand Down
14 changes: 14 additions & 0 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
- [Errors](#errors)
- [Fullscreen-mode](#fullscreen)
- [Генерация звука](#audio-generate)
- [MIDI](#midi)
- [Хранение состояния (настроек) интерфейса (Import / Export)](#state-storing)
- [Tech guide](#tech)
- [Secure context](#secure)
Expand Down Expand Up @@ -467,6 +468,19 @@ AMI состоит из виртуальных устройств (узлов),

**Генерация звука** — индикатор, показывающий генерируется ли в данный момент звук.

### MIDI <a name="midi"></a>

AMI позволяет генерировать MIDI-сообщения. Для включения MIDI необходимо включить MIDI-режим. Автоматически выбирается первый попавшийся порт из доступных и его первый канал. При превышении отсечки посылается сигнал noteOn, далее в режиме Пик сигнал noteOff посылается через время, указанное в поле Длительность; в режиме Полный, noteOff посылается только по окончании движения + через время в поле Длительность; при этом во время движения при влиянии скорости на громкость меняется давление на канал (то есть скорость во время движения равноценна изменению силы нажатия на клавишу). В режиме Пик скорость увеличивает Velocity. Кнопка Сброс MIDI посылает сигнал allSoundOff.

MIDI-сообщения могут посылаться:
- в соседние вкладки и окна браузеров, если они слушают MIDI (например, в веб-аналог [DX7](http://mmontag.github.io/dx7-synth-js))
- в DAW и прочие приложения, где есть виртуальные синтезаторы (то есть AMI может управлять, например, синтезатором в Ableton)
- во внешние устройства, поддерживающие MIDI и подключённые к компьютеру

> **Note**: После любых манипуляций в MIDI-портами (подключение/отключение/переподключение) необходимо полностью перезапустить браузер, закрыв все окна браузера если их несколько
> **Note**: MIDI-сообщения генерируются только на десктопе. Смартфон в этом режиме только посылает данные о движении и положении
### Хранение состояния (настроек) системы <a name="state-storing"></a>

При первом запуске приложения устанавливается состояние по-умолчанию.
Expand Down
62 changes: 62 additions & 0 deletions client/src/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,71 @@
<input type="number" min="0" max="1" slide="3" coefficient="2" step="0.01" name="compressor-release" class="compressor-release" />
</div>
</div>

<div class="midi module desktop">
<span class="midi-span">MIDI</span>
<div class="midi__container">
<span class="key">Port</span>
<select name="midi-port" class="midi__select-port">
</select>
</div>
<div class="midi__container">
<span class="key">Channel</span>
<select name="midi-channel" class="midi__select-channel">
<option value="0">1</option>
<option value="1">2</option>
<option value="2">3</option>
<option value="3">4</option>
<option value="4">5</option>
<option value="5">6</option>
<option value="6">7</option>
<option value="7">8</option>
<option value="8">9</option>
<option value="9">10</option>
<option value="10">11</option>
<option value="11">12</option>
<option value="12">13</option>
<option value="13">14</option>
<option value="14">15</option>
<option value="15">16</option>
</select>
</div>
<div class="midi__container slide">
<span class="key">Velocity</span>
<input type="number" min="0" max="127" slide="3" coefficient="0" step="1" name="midi-velocity" class="midi-velocity" />
</div>
<div class="midi__container slide">
<span class="key">Modulation</span>
<input type="number" min="0" max="127" slide="3" coefficient="0" step="1" name="midi-modulation" class="midi-modulation" />
</div>
<div class="midi__container slide">
<span class="midi-duration-key key"></span>
<input type="number" slide="3" coefficient="1" step="0.1" name="midi-duration" class="midi-duration" />
</div>
<div class="midi__container">
<span class="midi-reset-key key"></span>
<div class="radio-element">
<input type="radio" name="midi-reset" id="midi-reset" value="true" />
</div>
</div>
</div>
</div>

<div class="bottom">
<div class="midi-regime desktop">
<span class="midi-regime-span"></span>
<div class="midi__container">
<div class="radio-element">
<input type="radio" name="midi" id="midi-yes" value="true" />
<label class="midi-on" for="midi-yes"></label>
</div>
<div class="radio-element">
<input type="radio" name="midi" id="midi-no" value="false" checked />
<label class="midi-off" for="midi-no"></label>
</div>
</div>
</div>

<div class="shortcuts">
<span class="shortcuts-span"></span>
<div class="shortcuts__container">
Expand Down
19 changes: 5 additions & 14 deletions client/src/js/audio.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toFixedNumber } from './helpers'
import { notes, notesInit, pitchDetection } from './notes'
import { orientationToFrequency } from './helpers'
import { notesInit, pitchDetection } from './notes'
import { settings } from './settings'

export let audioContext = null
Expand Down Expand Up @@ -489,8 +489,8 @@ let frequency = null
let previousFrequency = null // To work more efficiently with DOM updates
const frequencyElement = document.querySelector('.motion__frequency')

const countElement = document.querySelector('.motion__count') // The number of oscillators
const isAudioElement = document.querySelector('.motion__is-audio')
export const countElement = document.querySelector('.motion__count') // The number of oscillators
export const isAudioElement = document.querySelector('.motion__is-audio')
const containerElement = document.querySelector('.container') // For animation by resetting oscillators

// All elements of batches are grouped into separate arrays
Expand Down Expand Up @@ -581,16 +581,7 @@ let previousMotionMaximum = 0

// The function is called ≈ every 16ms (in Chrome, in Firefox every 100ms)
export function audio(motion) {
// Define the frequency and the note
// Do it even below the cutoff, so we can hit the right note before sound synthesis begins
if (settings.audio.frequencyRegime === 'continuous') {
// Exponential — degrees of position in degree log range (value difference) on the basis of 180 (maximum gyroscope value) + minimum value
frequency = toFixedNumber(Math.pow(motion.orientation, Math.log(settings.audio.frequenciesRange.to - settings.audio.frequenciesRange.from) / Math.log(180)) + settings.audio.frequenciesRange.from, 4)
}
if (settings.audio.frequencyRegime === 'tempered') {
// Starting from 'from' in the chord upwards (to — from) notes by 180 degrees
frequency = notes[settings.audio.notesRange.from + Math.floor(motion.orientation * ((settings.audio.notesRange.to - settings.audio.notesRange.from) / 180))]
}
frequency = orientationToFrequency(motion.orientation)

// Update the DOM only when the value changes
if (previousFrequency !== frequency) {
Expand Down
28 changes: 27 additions & 1 deletion client/src/js/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { notes } from './notes'
import { settings } from './settings'

/**
* The function rounds the value to 4 decimal places by default
* @param {Number} number - number
Expand Down Expand Up @@ -45,4 +48,27 @@ export function getNearbyValues(number, array) {
export function getDate() {
let date = new Date()
return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
}
}

/**
* Convert motion to frequency
* @param {Object} motion - motion object from motion.js
* @return {Number} Returns the frequency
*/

let frequency = null
export function orientationToFrequency(orientation) {
// Define the frequency and the note
// Do it even below the cutoff, so we can hit the right note before sound synthesis begins
if (settings.audio.frequencyRegime === 'continuous') {
// Exponential — degrees of position in degree log range (value difference) on the basis of 180 (maximum gyroscope value) + minimum value
frequency = toFixedNumber(Math.pow(orientation, Math.log(settings.audio.frequenciesRange.to - settings.audio.frequenciesRange.from) / Math.log(180)) + settings.audio.frequenciesRange.from, 4)
}

if (settings.audio.frequencyRegime === 'tempered') {
// Starting from 'from' in the chord upwards (to — from) notes by 180 degrees
frequency = notes[settings.audio.notesRange.from + Math.floor(orientation * ((settings.audio.notesRange.to - settings.audio.notesRange.from) / 180))]
}

return frequency
}
16 changes: 14 additions & 2 deletions client/src/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ export const i18n = {
links: '<a href="https://github.com/MaxAlyokhin/audio-motion-interface" target="_blank" rel="noopener noreferrer">Исходники</a> <a href="https://github.com/MaxAlyokhin/audio-motion-interface/blob/master/README_RU.md#user" target="_blank" rel="noopener noreferrer">Инструкция</a>',
run: 'Запуск',
interface: 'Скрыть интерфейс',
slide: 'Перетащите кружок за черту для разблокировки интерфейса'
slide: 'Перетащите кружок за черту для разблокировки интерфейса',
noMIDIPortsFound: 'Не найдено ни одного MIDI-порта. Проверьте настройки MIDI. После подключения MIDI необходимо будет полностью перезагрузить браузер и попробовать снова.',
notesCount: 'Количество нот',
netLatency: 'Задержка связи (мс)',
duration: 'Длительность (с)',
midiReset: 'Сброс MIDI',
midiRegime: 'MIDI-режим'
},

en: {
Expand Down Expand Up @@ -148,6 +154,12 @@ export const i18n = {
links: '<a href="https://github.com/MaxAlyokhin/audio-motion-interface" target="_blank" rel="noopener noreferrer">Source code</a> <a href="https://github.com/MaxAlyokhin/audio-motion-interface/blob/master/README.md#user" target="_blank" rel="noopener noreferrer">User guide</a>',
run: 'Run',
interface: 'Interface off',
slide: 'Slide up the circle to the line for unlock interface'
slide: 'Slide up the circle to the line for unlock interface',
noMIDIPortsFound: 'No MIDI ports found. Check the MIDI settings. After After connecting MIDI, you will need to completely reboot the browser and try again.',
notesCount: 'Notes amount',
netLatency: 'Network latency (ms)',
duration: 'Duration (s)',
midiReset: 'Reset MIDI',
midiRegime: 'MIDI-regime'
}
}
5 changes: 5 additions & 0 deletions client/src/js/language.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ function setLanguage(languageMarker) {
document.querySelector('.shortcuts-off').textContent = language.no
document.querySelector('.latency__title').textContent = language.latency
document.querySelector('.is-audio').textContent = language.isAudio
document.querySelector('.midi-duration-key').textContent = language.duration
document.querySelector('.midi-reset-key').textContent = language.midiReset
document.querySelector('.midi-regime-span').textContent = language.midiRegime
document.querySelector('.midi-on').textContent = language.yes
document.querySelector('.midi-off').textContent = language.no

languageElements.forEach((element) => { element.style.textDecoration = 'none' })
document.querySelector(`.${languageMarker}`).style.textDecoration = 'underline'
Expand Down
3 changes: 1 addition & 2 deletions client/src/js/localstorage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { settings, syncSettingsFrontend } from "./settings"
import { settings } from "./settings"

// localStorage control
export function checkLocalStorage() {
Expand All @@ -9,7 +9,6 @@ export function checkLocalStorage() {
} else {
// Otherwise, loading the earlier settings from localStorage and rewriting
Object.assign(settings, JSON.parse(localStorage.getItem('settings')))
syncSettingsFrontend(settings)
}
}

Expand Down
24 changes: 13 additions & 11 deletions client/src/js/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import device from 'current-device'

import languageInit from './language'
import { audioInit } from './audio'
import { motionInit } from './motion'
import { midiInit } from './midi'
import { checkLocalStorage } from './localstorage'
import { settingsExchangeInit } from './settingsExchange'

Expand All @@ -19,26 +22,25 @@ window.addEventListener('load', () => {
function () {
document.querySelector('body').style.overflow = 'auto'

if (device.desktop()) midiInit()
audioInit()
motionInit()

},
{ once: true } // Only works once
)

document.querySelector('.button.run').addEventListener(
'click',
function () {
document.querySelector('.cover').style.opacity = 0
setTimeout(() => {
document.querySelector('.cover').style.display = 'none'
}, 1000)
}
)
document.querySelector('.button.run').addEventListener('click', function () {
document.querySelector('.cover').style.opacity = 0
setTimeout(() => {
document.querySelector('.cover').style.display = 'none'
}, 1000)
})

document.querySelector('.container .title span').addEventListener('click', () => {
document.querySelector('.cover').style.display = 'flex'
setTimeout(() => { document.querySelector('.cover').style.opacity = 1 })
setTimeout(() => {
document.querySelector('.cover').style.opacity = 1
})
})

// Displaying errors on the screen
Expand Down
Loading

0 comments on commit f8b1004

Please sign in to comment.