Реализация простого аудиоредактора в Интернете

JavaScript
Реализация простого аудиоредактора в Интернете

предисловие

На рынке существует множество программ для редактирования аудио, таких как Cubase, Sonar и так далее. Хотя они мощные, приложений в Интернете более чем достаточно. Поскольку большая часть ресурсов веб-приложений хранится на сетевом сервере, с программным обеспечением Cubase аудиофайлы должны быть сначала загружены, а затем загружены на сервер после модификации и, наконец, обновлены, что крайне неэффективно. Если звук можно редактировать непосредственно в Интернете и обновлять на сервере, это может значительно повысить эффективность работы операторов. Далее вы узнаете, как использовать веб-технологии для создания высокопроизводительного аудиоредактора.

Эта статья разделена на 3 главы:

  • Глава 1: Теоретическое знание звука
  • Глава 2: Как реализовать аудиоредактор
  • Глава 3: Оптимизация производительности аудиоредактора

Глава 1. Теория, связанная со звуком

Теория является основой и основой практики.Понимание теории может лучше помочь нам практиковать и решать проблемы, возникающие на практике.

1.1 Что такое звук

Когда предмет колеблется, он возбуждает колебание окружающих его частиц воздуха.Благодаря сжимаемости воздуха при взаимодействии частиц воздух вокруг колеблющегося предмета попеременно сжимается и расширяется, и постепенно распространяется наружу, образуя тем самым звуковые волны. Звуковые волны вводятся в ухо человека через среду (воздух, твердое тело, жидкость), которая приводит в колебание слуховые косточки, после прохождения серии нервных сигналов они воспринимаются человеком и образуют звук. Причина, по которой мы можем слышать звук музыкальных инструментов, таких как пианино, эрху и громкоговоритель, заключается в том, что определенные компоненты музыкального инструмента генерируют звуковые волны посредством вибрации, которые передаются нашим ушам по воздуху.

1.2 Звуковой фактор

Почему голоса людей различаются, и почему голоса одних людей звучат красиво, а голоса других противны? В этом разделе представлены 3 основных фактора звука: частота, амплитуда и тембр.После понимания этих факторов каждый поймет, почему.

1.2.1 Частота

Поскольку звук представляет собой звуковую волну, он имеет амплитуду и частоту. Чем выше частота и чем выше высота тона, тем резче звук.Например, женские голоса обычно громче мужских, поэтому их голоса будут более резкими. Человеческое ухо обычно слышит звуковые волны только в диапазоне частот от 20 Гц до 20 кГц.

1.2.2 Амплитуда

Когда звуковые волны распространяются по воздуху, воздух, проходящий через них, попеременно сжимается и расширяется, вызывая изменения атмосферного давления. Чем больше амплитуда, тем больше изменение атмосферного давления, и тем громче звуковая волна будет слышна человеческому уху. Диапазон звукового давления (звуковое давление: изменение атмосферного давления, вызванное звуковыми волнами) для человеческого уха составляет (2 * 10 ^ - 5) Па ~ 20 Па, а соответствующий децибел составляет 0 ~ 120 дБ. Формула преобразования между ними20 * log( X / (2 * 10 ^ -5) ), где X — звуковое давление. По сравнению с атмосферным давлением для выражения интенсивности звуковой амплитуды более интуитивно понятно выражать ее в децибелах. Когда мы обычно описываем силу звука объекта, мы обычно используем децибелы вместо того, чтобы сказать, сколько Паскалей издает звуковое давление громкоговорителя (но звучит очень мощно).

1.2.3 Голос

Частота и амплитуда не являются главными факторами, определяющими, убогий или приятный голос человека, главным фактором, определяющим, приятный ли голос человека, является тембр, который определяется гармониками в звуковой волне. Звуковые волны, генерируемые вибрацией объектов в природе, не являются волнами одной частоты и одной амплитуды (например, синусоида), а могут быть разложены на основную волну плюс бесчисленное количество гармоник. Основная волна и гармоническая волна представляют собой синусоидальные волны, в которых частота гармонической волны является целым числом, кратным основной волне, амплитуда меньше основной волны, а фаза также отличается. Например, у центрального доу фортепиано его основная частота равна 261, а другие бесчисленные гармонические частоты кратны 261. Человек с хорошим голосом произносит «хорошие» гармоники при произнесении, а человек с плохим голосом будет производить более «уродливые» гармоники.

1.3 Запись, редактирование и воспроизведение звука

Будь то европейские и американские фортепиано, скрипки или китайские соны, эрху и громкоговорители, мы не можем просить исполнителей играть для нас вживую, когда мы хотим их услышать. слушаю. Традиционный метод звукозаписи заключается в преобразовании вибрации звука в аналоговый электрический ток через такое устройство, как микрофон, его усиление и обработка, а затем запись на ленту или передача на устройство, такое как динамик для звука. . Этот метод имеет большие искажения, и его трудно устранить шум, и его нелегко редактировать и изменять.Цифровая технология может помочь нам решить проблемы, вызванные аналоговым током. В этом разделе мы расскажем, как это делают цифровые технологии.

1.3.1 Запись

Звук — это непрерывная и нерегулярная звуковая волна, состоящая из бесчисленных синусоидальных волн. Процесс цифровой записи заключается в сборе амплитуд дискретных точек в этой звуковой волне, их квантовании, кодировании и сохранении в компьютере. Основной принцип всего процесса: после того, как звук проходит через микрофон, формируется непрерывный сигнал изменения напряжения в соответствии с различными амплитудами, в это время дискретные изменения напряжения собираются импульсным сигналом, и, наконец, собранные результаты квантуются, кодируются и сохраняются в компьютере. Частота импульса дискретизации обычно составляет 44,1 кГц, потому что человеческое ухо может слышать только синусоидальную часть звуковой волны с частотой 20-20 кГц.Согласно закону дискретизации, чтобы полностью восстановить исходную форму волны из последовательности значений дискретизации, частота дискретизации должен быть больше или равен исходной 2-кратной максимальной частоте сигнала. Поэтому, если вы хотите сохранить все синусоидальные волны в пределах 20 кГц от исходной звуковой волны, частота дискретизации должна быть больше или равна 40 кГц.

1.3.2 Редактировать

После того, как звук оцифрован, очень удобно редактировать звук, например, отображать форму звуковой волны, перехватывать звук, добавлять эффект приглушения, нарастание и затухание, а также просматривать звуковую спектрограмму (диаграмму распределения каждой гармоники) через дискретное преобразование Фурье или операции фильтрации (для фильтрации нежелательных гармонических частей), эти кажущиеся сложными операции могут быть выполнены только с помощью простых вычислений на квантованных данных.

1.3.3 Воспроизведение

Процесс воспроизведения является процессом, обратным процессу записи: записанные или отредактированные аудиоданные декодируются, квантуются и преобразуются в дискретные сигналы напряжения и передаются на громкоговоритель. Как громкоговоритель восстанавливает сигнал напряжения до определенной амплитуды звуковой волны, это не углубленное исследование, только здесь.

Глава 2. Как реализовать аудиоредактор

Благодаря теоретическим знаниям, изложенным в главе 1, мы знаем, что такое звук, а также запись и воспроизведение звука.Записанные и сохраненные звуковые данные называются звуком, и, редактируя звуковые данные, мы можем получить желаемый звуковой эффект воспроизведения. В этой главе мы начнем знакомить вас с тем, как реализовать инструменты редактирования аудио с помощью браузеров. Браузер предоставляет объект AudioContext для обработки аудиоданных.В этой главе сначала будут представлены основы использования AudioContext, а затем будет рассказано, как использовать svg для рисования аудиосигналов и как редактировать аудиоданные.

2.1 Введение в AudioContext

Процесс обработки аудиоданных в AudioContext представляет собой потоковый процесс, начиная со сбора аудиоданных, обработки данных и воспроизведения аудиоданных, пошаговой потоковой передачи. Объект AudioContext предоставляет методы и свойства, необходимые для потоковой обработки.Например, метод context.createBufferSource возвращает узел буфера аудиоданных для хранения аудиоданных, который является начальной точкой всей потоковой передачи, а свойство context.destination является конечной точкой. всей потоковой передачи для воспроизведения аудио. Каждый метод возвращает объект узла AudioNode, и все узлы AudioNode подключаются через метод AudioNode.connect.

Ниже приведен простой пример разблокировки AudioContext:

  • Для удобства вместо использования аудиофайла на сервере мы используем FileReader для чтения локального аудиофайла.
  • Используйте метод decodeAudioData AudioContext для декодирования прочитанных аудиоданных.
  • Создайте узел источника звука, используя метод createBufferSource AudioContext, и назначьте ему декодированный результат.
  • Подключите узел источника звука к конечной точке воспроизведения, используя метод подключения AudioContext — свойство назначения AudioContext.
  • Используйте метод запуска AudioContext, чтобы начать воспроизведение
    // 读取音频文件.mp3 .flac .wav等等
    const reader = new FileReader();
    // file 为读取到的文件,可以通过<input type="file" />实现
    reader.readAsArrayBuffer(file);
    reader.onload = evt => {
        // 编码过的音频数据
        const encodedBuffer = evt.currentTarget.result;
        // 下面开始处理读取到的音频数据
        // 创建环境对象
        const context = new AudioContext();
        // 解码
        context.decodeAudioData(encodedBuffer, decodedBuffer => {
            // 创建数据缓存节点
            const dataSource = context.createBufferSource();
            // 加载缓存
            dataSource.buffer = decodedBuffer;
            // 连接播放器节点destination,中间可以连接其他节点,比如音量调节节点createGain(),
            // 频率分析节点(用于傅里叶变换)createAnalyser()等等
            dataSource.connect(context.destination);
            // 开始播放
            dataSource.start();
        })
    }

2.1 Что такое звуковая волна

Редактор аудио визуализирует аудиоданные через форму аудиосигнала.Пользователи могут получить соответствующие аудиоданные только путем редактирования формы аудиосигнала.Конечно, внутренняя реализация заключается в преобразовании операции формы волны в операцию аудиоданных. Так называемая звуковая волна представляет собой изменение амплитуды звука (звуковой волны) со временем во временной области, то есть ось X — это время, а ось Y — амплитуда.

2.2 Рисование сигналов

Мы знаем, что звук дискретизируется с частотой 44,1 кГц, поэтому 10-минутный аудиосеанс будет иметь в общей сложности 10 * 60 * 44100 = 26460000, более 25 миллионов точек данных. Когда мы рисуем сигнал, даже если для представления амплитуды одной точки используется только 1 пиксель, ширина сигнала составляет почти 25 миллионов пикселей, что не только медленно рисуется, но и очень неблагоприятно для анализа сигнала. Поэтому ниже вводится алгоритм аппроксимации для уменьшения отрисовываемых пикселей: мы сначала делим собираемые каждую секунду 44100 точек на 100 частей, что эквивалентно 10 миллисекундам, и каждая часть имеет 441 точку, Найдите их максимальное и минимальное значения. Используйте максимальное значение для представления пиков и минимальное значение для представления впадин, затем соедините все пики и впадины линией. После квантования аудиоданных диапазон значений составляет [-1,1], Следовательно, все пики и впадины, которые мы здесь берем, находятся в интервале [-1,1]. Так как значение слишком маленькое, нарисованная осциллограмма некрасивая.Умножаем эти значения на коэффициент типа 64, чтобы можно было четко наблюдать изменения осциллограммы. Вы можете использовать холст или svg для рисования сигналов.Здесь я предпочитаю использовать svg для рисования, потому что svg — это векторная диаграмма, которая может упростить алгоритм масштабирования сигналов.

Код

  • Чтобы облегчить использование svg для рисования, введитеsvg.jsи инициализируйте отрисовку объекта svg
  • Наш алгоритм рисования делит 44 100 точек, собираемых каждую секунду, на 100 равных частей, каждая из которых составляет 10 миллисекунд с общим количеством точек данных 441, и использует их максимальные и минимальные значения в качестве пиков и минимумов в этот момент времени. Затем используйте svg.js, чтобы соединить все пики и впадины с помощью полилинии, чтобы сформировать окончательную форму волны. Поскольку точки аудиоданных квантуются, а диапазон равен [-1,1], чтобы сделать форму волны более красивой, мы Пики и впадины будут равномерно умножены на коэффициент усиления, чтобы увеличить амплитуду ломаных линий.
  • Переменная инициализации perSecPx (количество пикселей, отрисовываемых в секунду) равна 100, а коэффициент увеличения высоты пиков и впадин равен 128.
  • Получите все пики точек данных пиков и впадин за 10 миллисекунд, а метод расчета заключается в простом вычислении их соответствующих максимальных и минимальных значений.
  • Инициализировать ширину сигнала svgWidth = длительность звука (buff.duration) * количество пикселей, отрисовываемых в секунду (perSecPx)
  • Пересеките пики, умножьте все пики и впадины на коэффициенты и соедините их ломаной линией.
const SVG = require('svg.js');
// 创建svg对象
const draw = SVG(document.getElementById('draw'));
// 波形svg对象
let polyline;
// 波形宽度
let svgWidth;
// 展示波形函数
// buffer - 解码后的音频数据
function displayBuffer(buff) {
    // 每秒绘制100个点,就是将每秒44100个点分成100份,
    // 每一份算出最大值和最小值来代表每10毫秒内的波峰和波谷
    const perSecPx = 100;
    // 波峰波谷增幅系数
    const height = 128;
    const halfHight = height / 2;
    const absmaxHalf = 1 / halfHight;
    // 获取所有波峰波谷
    const peaks = getPeaks(buff, perSecPx);
    // 设置svg的宽度
    svgWidth = buff.duration * perSecPx;
    draw.size(svgWidth);
    const points = [];
    for (let i = 0; i < peaks.length; i += 2) {
        const peak1 = peaks[i] || 0;
        const peak2 = peaks[i + 1] || 0;
        // 波峰波谷乘上系数
        const h1 = Math.round(peak1 / absmaxHalf);
        const h2 = Math.round(peak2 / absmaxHalf);
        points.push([i, halfHight - h1]);
        points.push([i, halfHight - h2]);
    }
    // 连接所有的波峰波谷
    const  polyline = draw.polyline(points);
    polyline.fill('none').stroke({ width: 1 });
}
// 获取波峰波谷
function getPeaks(buffer, perSecPx) {
    const { numberOfChannels, sampleRate, length} = buffer;
    // 每一份的点数=44100 / 100 = 441
    const sampleSize = ~~(sampleRate / perSecPx);
    const first = 0;
    const last = ~~(length / sampleSize)
    const peaks = [];
    // 为方便起见只取左声道
    const chan = buffer.getChannelData(0);
    for (let i = first; i <= last; i++) {
        const start = i * sampleSize;
        const end = start + sampleSize;
        let min = 0;
        let max = 0;
        for (let j = start; j < end; j ++) {
            const value = chan[j];
            if (value > max) {
                max = value;
            }
            if (value < min) {
                min = value;
            }
        }
    }
    // 波峰
    peaks[2 * i] = max;
    // 波谷
    peaks[2 * i + 1] = min;
    return peaks;
}

2.3 Масштабирование

Иногда необходимо увеличить масштаб отдельных областей или уменьшить масштаб всей формы волны. Поскольку звуковая волна рисуется с помощью svg, алгоритм масштабирования становится очень простым, просто масштабируйте svg напрямую.

Код

  • Используя функцию векторной диаграммы svg, нам нужно только умножить ширину полилинии, соединяющей долины WDM, на коэффициент scaleX, чтобы реализовать функцию масштабирования. На самом деле это своего рода псевдозум, потому что точность осциллограммы всегда составляет 10 миллисекунд, и он просто уводит линейный график.
function zoom(scaleX) {
    draw.width(svgWidth * scaleX);
    polyline.width(svgWidth * scaleX);
}

2.4 Операция обрезки

В этом разделе в основном представлена ​​реализация операции обрезки, а другие операции аналогичны вычислению аудиоданных. Так называемая обрезка предназначена для удаления ненужных частей из исходного звука, таких как части шума, или для вырезания нужных частей, таких как часть припева. Чтобы реализовать обрезку аудиофайлов, Прежде всего, нам нужно достаточно знать об этом. Декодированные аудиоданные на самом деле являютсяAudioBufferобъект, он будет назначенAudioBufferSourceNodeСвойство буфера узла аудиоисточника, определяемое AudioBufferSourceNode. Внесите его в поток обработки AudioContext, где узлы AudioBufferSourceNode могут быть созданы с помощью метода createBufferSource AudioContext. Учащиеся, которые немного запутались, могут вернуться к разделу 2.1, чтобы рассмотреть основы использования AudioContext. Объекты AudioBuffer имеют sampleRate (частота дискретизации, обычно 44,1 кГц), numberOfChannels (каналномер), Есть 4 свойства duration (длительность), length (длина данных) и важный метод getChannelData, возвращающий массив типа Float32Array. Мы просто меняем данные в этом массиве Float32Array на правильные. Обрезка аудио или другие операции. Конкретные этапы резки:

  • Сначала получите количество каналов и частоту дискретизации аудио для обработки.
  • Вычислите длину отрезка в соответствии со временем начала, временем окончания и частотой дискретизации отрезка: length lengthInSamples = (endTime - startTime) * sampleRate, а затем передайте AudioContextcreateBufferМетод создает AudioBuffer с длиной lengthInSamples cutAudioBuffer для хранения обрезанных аудиоданных, а затем создает AudioBuffer с длиной исходной длины аудио минус lengthInSamples newAudioBuffer используется для хранения обрезанных аудиоданных.
  • Поскольку звук часто является многоканальным, операция отсечения должна обрезать все каналы, поэтому мы проходим по всем каналам и возвращаем аудиоданные типа Float32Array каждого канала через метод getChannelData AudioBuffer.
  • Получите аудиоданные, которые необходимо вырезать, с помощью метода подмассива Float32Array, установите для данных значение cutAudioBuffer с помощью метода set и одновременно установите для вырезанных аудиоданных значение newAudioBuffer.
  • вернуть newAudioBuffer и вырезатьAudioBuffer
function cut(originalAudioBuffer, start, end) {
    const { numberOfChannels, sampleRate } = originalAudioBuffer;
    const lengthInSamples = (end - start) * sampleRate;
    // offlineAudioContext相对AudioContext更加节省资源
    const offlineAudioContext = new OfflineAudioContext(numberOfChannels, numberOfChannels, sampleRate);
    // 存放截取的数据
    const cutAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        lengthInSamples,
        sampleRate
    );
    // 存放截取后的数据
    const newAudioBuffer = offlineAudioContext.createBuffer(
        numberOfChannels,
        originalAudioBuffer.length - cutSegment.length,
        originalAudioBuffer.sampleRate
    );
    // 将截取数据和截取后的数据放入对应的缓存中
    for (let channel = 0; channel < numberOfChannels; channel++) {
        const newChannelData = newAudioBuffer.getChannelData(channel);
        const cutChannelData = cutAudioBuffer.getChannelData(channel);
        const originalChannelData = originalAudioBuffer.getChannelData(channel);
        const beforeData = originalChannelData.subarray(0,
            start * sampleRate - 1);
        const midData = originalChannelData.subarray(start * sampleRate,
            end * sampleRate - 1);
        const afterData = originalChannelData.subarray(
            end * sampleRate
        );
        cutChannelData.set(midData);
        if (start > 0) {
            newChannelData.set(beforeData);
            newChannelData.set(afterData, (start * sampleRate));
        } else {
            newChannelData.set(afterData);
        }
    }
    return {
        // 截取后的数据
        newAudioBuffer,
        // 截取部分的数据
        cutSelection: cutAudioBuffer
    };
};

2.5 Отмена и повтор операций

Перед каждой операцией сохраняйте текущие аудиоданные. При отмене или повторе загрузите в него соответствующие аудиоданные. Этот метод имеет большие накладные расходы, которые анализируются в Главе 3 — Оптимизация производительности.

Глава 3. Оптимизация производительности аудиоредактора

3.1 Существующие проблемы

Используя метод аппроксимации, представленный в главе 2, для построения звуковой волны с меньшим количеством точек, функция просмотра формы волны была в основном удовлетворена. Но есть еще следующие 2 проблемы с производительностью:

  1. Если сигнал масштабируется и анализируется, например, при увеличении сигнала в 10 и более раз, даже если сигнал, нарисованный в формате svg, может быть адаптивно увеличен без искажения, поскольку весь сигнал увеличивается более чем в 10 раз, число пикселей, которые необходимо отрисовать, также увеличивается в 10 раз, в результате чего весь процесс масштабирования становится очень запаздывающим.
  2. Функция отмены и повтора Это требует сохранения измененных аудиоданных для каждой операции. Фрагмент аудиоданных обычно составляет от нескольких мегабайт до десятка мегабайт, и если вы будете сохранять его каждый раз при работе, это неизбежно приведет к разрыву памяти.

3.2 Схема оптимизации производительности

3.2.1 Ленивая загрузка

Основная причина, по которой увеличенная форма волны застревает, заключается в том, что нужно отрисовать слишком много пикселей, поэтому мы можем уменьшить количество пикселей, которые необходимо отрисовывать каждый раз, когда волна отрисовывается с помощью отложенной загрузки. Конкретное решение заключается в том, что в соответствии с положением прокрутки текущего сигнала диапазон сигнала, который должен быть отрисован в текущем окне просмотра, рассчитывается в реальном времени. Поэтому необходимо модифицировать функцию getPeaks для получения пиков и впадин в главе 2, добавив 2 параметра:

  • буфер: декодированные аудиоданные AudioBuffer
  • pxPerSec: количество пикселей по горизонтали, необходимых для аудиоданных в секунду, здесь 100, и каждые 10 миллисекунд данных соответствуют 1 группе пиков и спадов.
  • start: начальная позиция прокрутки текущего окна просмотра осциллограммы scrollLeft
  • end: scrollLeft + viewWidth конечной позиции прокрутки текущего окна просмотра сигнала.
  • В конкретном расчете мы берем только пики и провалы звука в соответствующий период времени в текущем окне просмотра.
  • Например, начало равно 10, а конец равен 100. В соответствии с нашим приблизительным алгоритмом 1 пиксель, соответствующий 1 пику и впадине объема данных 10 миллисекунд, мы берем пик и впадину 10-х 10 миллисекунд в сотые 10 миллисекунд, то есть период времени составляет от 100 миллисекунд до 1 секунды.
function getPeaks(buffer, pxPerSec, start, end) {
    const { numberOfChannels, sampleRate } = buffer;
    const sampleWidth = ~~(sampleRate / pxPerSec);
    const step = 1;
    const peaks = [];
    for (let c = 0; c < numberOfChannels; c++) {
        const chanData = buffer.getChannelData(c);
        for (let i = start, z = 0; i < end; i += step) {
            let max = 0;
            let min = 0;
            for (let j = i * sampleWidth; j < (i + 1) * sampleWidth; j++) {
                const value = chanData[j];
                max = Math.max(value, max);
                min = Math.min(value, min);
            }
            peaks[z * 2] = Math.max(max, peaks[z * 2] || 0);
            peaks[z * 2 + 1] = Math.min(min, peaks[z * 2 + 1] || 0);
            z++;
        }
    }
    return peaks;
}

3.2.2 Оптимизация операций отмены

По сути, нам нужно только сохранить копию исходных необработанных аудиоданных, а затем перед каждым редактированием сохранить все выполняемые в данный момент наборы инструкций, сделать это один раз. Например: выполнить 2 операции над осциллограммой: в первой операции отрезать часть 0-1 секунды, сохранить набор команд А как вырезку 0-1 секунды; во второй операции вырезать часть 2-3 секунд снова, сохраните набор инструкций B как резка 0-1 секунд, резка 2-3 секунд. Чтобы отменить вторую операцию, просто используйте предыдущий набор инструкций A для выполнения одной операции с исходным сигналом. Сохраняя таким образом набор инструкций, потребление памяти значительно снижается.

Суммировать

Суть звука в том, что звуковые волны колеблются в человеческом ухе и воспринимаются человеческим мозгом.К факторам, определяющим качество звука, относятся амплитуда, частота и тембр (гармоники).Человеческое ухо может распознавать звуки только с частотами 20- 20 кГц и амплитуды 0-120 дБ. Процесс цифровой обработки звука включает в себя: выборку импульсов, квантование, кодирование, декодирование, обработку и воспроизведение. При использовании холста или svg для рисования звуковых сигналов производительность резко падает по мере увеличения количества отображаемых пикселей.Благодаря отложенной загрузке рисования по требованию производительность рисования может быть эффективно улучшена. Операции отмены и повтора выполняются путем сохранения набора инструкций, что позволяет эффективно экономить потребление памяти. Есть еще много вещей, которые может сделать API веб-аудио, и я с нетерпением жду совместной работы.

Ссылаться на

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!