Skip to content

Commit

Permalink
Add note detection
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxAlyokhin committed May 20, 2022
1 parent ecd60c2 commit 61aafa1
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 22 deletions.
112 changes: 90 additions & 22 deletions client/src/js/audio.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getNearbyValues, toFixedNumber } from './helpers'
import { getNoteName, notes, notesInit, pitchDetection } from './notes'
import { settings } from './settings'

let audioContext = undefined
Expand All @@ -9,6 +11,17 @@ try {
window.alert(`Браузер не поддерживается / Browser is not support`)
}

// Определяем массив нот в рамках равномерной темперации
notesInit()

// Генерируемая частота звука и html-элемент, куда будем её записывать
let frequency = null
let frequencyElement = undefined

window.addEventListener('DOMContentLoaded', () => {
frequencyElement = document.querySelector('.motion__frequency')
})

// Схема: осциллятор => фильтр => громкость

// Режим множественных осцилляторов (каждому событию движения соответствует свой осциллятор)
Expand All @@ -24,7 +37,6 @@ function runOscillator(motionParameter) {
let biquadFilter = audioContext.createBiquadFilter()
let gainNode = audioContext.createGain()

// TODO: все осцилляторы должны подключаться к одному внешнему gainNode
oscillator.connect(biquadFilter)
biquadFilter.connect(gainNode)
gainNode.connect(audioContext.destination)
Expand All @@ -33,8 +45,12 @@ function runOscillator(motionParameter) {
biquadFilter.gain.value = 1

// Настраиваем по настройкам и акселерометру

frequency = toFixedNumber(motionParameter * settings.audio.frequencyFactor, 1)
frequencyElement.innerText = frequency

oscillator.type = settings.audio.oscillatorType
oscillator.frequency.value = motionParameter * settings.audio.frequencyFactor
oscillator.frequency.value = frequency
// oscillator.frequency.exponentialRampToValueAtTime(previousFrequency, currentTime + settings.audio.toneDuration)
previousFrequency = oscillator.frequency.value

Expand Down Expand Up @@ -71,33 +87,39 @@ function plural(motion) {

let oscillatorIsInit = false

// Объявляем сущности как var, чтобы они были видны в пределах всей функции
let currentTime = audioContext.currentTime // по идее это не нужно здесь
let oscillator = audioContext.createOscillator()
let biquadFilter = audioContext.createBiquadFilter()
let gainNode = audioContext.createGain()

oscillator.connect(biquadFilter)
biquadFilter.connect(gainNode)
gainNode.connect(audioContext.destination)

biquadFilter.type = 'lowpass'
biquadFilter.gain.value = 1
let currentTime = undefined
let oscillator = undefined
let biquadFilter = undefined
let gainNode = undefined

function single(motion) {
// Включаем осциллятор, когда движение превысило отсечку
if (motion.isMotion) {
// Собираем связку, делаем это только один раз в начале движения
if (!oscillatorIsInit) {
currentTime = audioContext.currentTime
oscillator = audioContext.createOscillator()
biquadFilter = audioContext.createBiquadFilter()
gainNode = audioContext.createGain()

oscillator.connect(biquadFilter)
biquadFilter.connect(gainNode)
gainNode.connect(audioContext.destination)

biquadFilter.type = 'lowpass'
biquadFilter.gain.value = 1

oscillator.start()
oscillatorIsInit = true
}

// Динамически настраиваем связку
// Эти данные обновляются по каждому событию движения
frequency = toFixedNumber(motion.maximum * settings.audio.frequencyFactor, 1)
frequencyElement.innerText = frequency

oscillator.type = settings.audio.oscillatorType
// TODO: выбрать: частота меняется плавно или сразу
oscillator.frequency.value = motion.maximum * settings.audio.frequencyFactor
oscillator.frequency.value = frequency
oscillator.frequency.exponentialRampToValueAtTime(previousFrequency, currentTime + settings.audio.toneDuration)

previousFrequency = oscillator.frequency.value
Expand All @@ -109,10 +131,54 @@ function single(motion) {
// Гасим осциллятор и удаляем всю связку устройств, когда движение опустилось ниже отсечки
else {
// Делаем это только если осциллятор есть и работает
// if (oscillatorIsInit) {
// oscillator.stop()
// oscillatorIsInit = false
// }
if (oscillatorIsInit) {
oscillator.stop()
oscillatorIsInit = false
}
}
}

function spatial(motion) {
if (motion.isMotion) {
// Собираем связку, делаем это только один раз в начале движения
if (!oscillatorIsInit) {
currentTime = audioContext.currentTime
oscillator = audioContext.createOscillator()
biquadFilter = audioContext.createBiquadFilter()
gainNode = audioContext.createGain()

oscillator.connect(biquadFilter)
biquadFilter.connect(gainNode)
gainNode.connect(audioContext.destination)

biquadFilter.type = 'lowpass'
biquadFilter.gain.value = 1

oscillator.start()
oscillatorIsInit = true
}

// Динамически настраиваем связку
// Эти данные обновляются по каждому событию движения
frequency = toFixedNumber(motion.orientation * settings.audio.frequencyFactor, 1)
frequencyElement.innerText = frequency

pitchDetection(frequency)

oscillator.type = settings.audio.oscillatorType
oscillator.frequency.value = frequency
// oscillator.frequency.exponentialRampToValueAtTime(previousFrequency, currentTime + settings.audio.toneDuration)

// previousFrequency = oscillator.frequency.value

biquadFilter.frequency.value = settings.audio.biquadFilterFrequency
// gainNode.gain.value = settings.audio.gain
// gainNode.gain.exponentialRampToValueAtTime(settings.audio.attenuation, +new Date() + settings.audio.toneDuration) // Затухание сигнала
} else {
if (oscillatorIsInit) {
oscillator.stop()
oscillatorIsInit = false
}
}
}

Expand All @@ -124,6 +190,8 @@ export function audio(motion) {
if (settings.audio.oscillatorRegime === 'single') {
single(motion)
}
}

// TODO: можно гироскоп привязать к бикубическому фильтру
if (settings.audio.oscillatorRegime === 'spatial') {
spatial(motion)
}
}
26 changes: 26 additions & 0 deletions client/src/js/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Функция округляет значение до 4 знаков после запятой по-умолчанию
// Принимает число и количество знаков после запятой
// Отдаёт округлённое число
export function toFixedNumber(number, digits = 4) {
let pow = Math.pow(10, digits)
return Math.round(number * pow) / pow
}

// Функция выполняет целочисленное деление
// Принимает два числа, что на что делить
// Отдаёт искомое число
export function div(value, by) {
return (value - (value % by)) / by
}

// Функция ищет ближайшее меньшее и большее число к заданному
// Принимает:
// - число, вокруг которого нужно найти ближайшие значения
// - массив чисел, из которых выбираем ближайшие значения
// Отдаёт массив из двух чисел: меньшее и большее
export function getNearbyValues(number, array) {
const nearbyLess = Math.max(...array.filter((value) => value < number))
const nearbyOver = Math.min(...array.filter((value) => value > number))

return [nearbyLess, nearbyOver]
}
55 changes: 55 additions & 0 deletions client/src/js/notes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Функция генерирует звукоряд

// Принимает:
// root - базовая частота, самая низкая частота
// octaveAmount - количество октав
// tonesInOctaveAmount - количество тонов (нот) в октаве

// Отдаёт:
// const tonesArray[] - массив тонов (звукоряд)

import { div, getNearbyValues, toFixedNumber } from './helpers'

export const notes = []

export function notesInit(root = 8.1757, octaveAmount = 12, tonesInOctaveAmount = 12) {
let tonesAmount = octaveAmount * tonesInOctaveAmount // Количество тонов

for (let i = 0; i < tonesAmount; i++) {
notes[i] = toFixedNumber(root * 2 ** (i / tonesInOctaveAmount)) // Формируем равномерно темперированный звукоряд
}
}

// Функция преобразует индекс массива notes[] в название ноты

const notesNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

export function getNoteName(frequency) {
// Находим индекс в массиве нот
const notesIndex = notes.indexOf(frequency)

// Если такого значения в массиве нет, значит мы звучим за диапазоном нот
if (notesIndex < 0) {
return `За диапазоном нот`
}

// Номер октавы
const octave = div(notesIndex, 12) - 1

// Порядковый номер ноты в рамках октавы
// Например, D == 3 (C - C# - D)
const noteNumberOnOctave = notesIndex + 1 - 12 * (octave + 1)

// Собираем название ноты вместе с номером октавы
const noteName = notesNames[noteNumberOnOctave - 1] + String(octave)

return noteName
}

// Функция приводит звучащий звук к ближайшей ноте
// pitchCorrection
export function pitchDetection(frequency) {
const nearbyValues = getNearbyValues(frequency, notes)
console.log(nearbyValues[0], nearbyValues[1])
console.log(getNoteName(nearbyValues[0]), getNoteName(nearbyValues[1]))
}

0 comments on commit 61aafa1

Please sign in to comment.