Всё началось с того, что я задумался о том, как отобразить на сайте информацию о загруженном аудио-файле. Для начала решил разобраться с самым простым форматом — wav. Как оказалось, ничего сложного в этом нет и писать именно об этом, в общем-то, не было бы никакого смысла, благо, информации о том, как устроен wav-файл «изнутри» в Интернете полно.
И тут Остапа понесло И тут в голову пришла светлая мысль о том, что было бы прикольно не просто отображать информацию о файле, но и иметь возможность генерировать такой файл «на лету». Думаю, все видели в сети всевозможные «онлайн-пианино» и прочее, верно?
Итак, что мне удалось сделать за 2 вечера — под катом.
Итак, для начала всё-таки вернемся к структуре WAV-файла, как такового. Для простоты берем самый просто одноканальный wav-файл без сжатия.
Любой wav-файл состоит из нескольких секций (чанков, chunks). Подробно обо всех секциях можно почитать, например, по ссылке, я же остановлюсь на трёх основных:
- секция типа — «RIFF»
- секция формата — «fmt „
- секция данных — “data»
У каждой секции есть её ID, размер секции и, собственно, какие-то данные, специфичные для данной секции.
Секция RIFF проста до безобразия: «RIFF<размер файла — 8>WAVE»
<размер файла — 8> потому что это значение характеризует «сколько байт содержится далее». Соответственно, 4 байта на само значение «сколько» и еще 4 на «RIFF» который был в начале.
В секции формата хранится основная интересующая обычного человека информация о файле: Sample Rate (частота дискретизации, например 44100 Гц), количество каналов (1 = моно, 2 = стерео и так далее).
В секции данных, собственно, и лежат нужные нам для проигрывания аудио-данные. По сути, они из себя представляют амплитуду волны в момент времени.
Исходя из всего вышесказанного и исходя из спецификации самого формата, ничего нам не мешает написать простейшие классы, описывающие каждую нужную нам секцию и простейший парсер, который будет считывать wav-файл и создавать необходимые нам объекты.
class Header { ... /** * @var string */ protected $id; /** * @var int */ protected $size; /** * @var string */ protected $format; ...
class FormatSection { ... /** * @var string */ protected $id; /** * @var int */ protected $size; /** * @var int */ protected $audioFormat; /** * @var int */ protected $numberOfChannels; /** * @var int */ protected $sampleRate; /** * @var int */ protected $byteRate; /** * @var int */ protected $blockAlign; /** * @var int */ protected $bitsPerSample; ...
class DataSection { ... /** * @var string */ protected $id; /** * @var int */ protected $size; /** * @var int[] */ protected $raw; ...
В коде выше убрана вся логика, нам сейчас интересна только структура самих данных.
Собственно, для их чтения сделаем небольшую обёртку-helper для fread для более удобного чтения именно бинарных данных.
class Helper { ... public static function readString($handle, $length) { return self::readUnpacked($handle, 'a*', $length); } public static function readLong($handle) { return self::readUnpacked($handle, 'V', 4); } public static function readWord($handle) { return self::readUnpacked($handle, 'v', 2); } protected function readUnpacked($handle, $type, $length) { $data = unpack($type, fread($handle, $length)); return array_pop($data); } ... }
Осталось дело за малым, взять и прочитать содержимое wav-файла:
class Parser { ... public static function fromFile($filename) { ... $handle = fopen($filename, 'rb'); try { $header = Header::createFromArray(self::parseHeader($handle)); $formatSection = FormatSection::createFromArray(self::parseFormatSection($handle)); $dataSection = DataSection::createFromArray(self::parseDataSection($handle)); } finally { fclose($handle); } return new AudioFile($header, $formatSection, $dataSection); } protected static function parseHeader($handle) { return [ 'id' => Helper::readString($handle, 4), 'size' => Helper::readLong($handle), 'format' => Helper::readString($handle, 4), ]; } protected static function parseFormatSection($handle) { return [ 'id' => Helper::readString($handle, 4), 'size' => Helper::readLong($handle), 'audioFormat' => Helper::readWord($handle), 'numberOfChannels' => Helper::readWord($handle), 'sampleRate' => Helper::readLong($handle), 'byteRate' => Helper::readLong($handle), 'blockAlign' => Helper::readWord($handle), 'bitsPerSample' => Helper::readWord($handle), ]; } protected static function parseDataSection($handle) { $data = [ 'id' => Helper::readString($handle, 4), 'size' => Helper::readLong($handle), ]; if ($data['size'] > 0) { $data['raw'] = fread($handle, $data['size']); } return $data; }
Итак, данные получены, мы их можем вывести в нужном на месте простым исполнением чего-то в духе:
echo $audio->getSampleRate();
Создание wav-файлов
Итак, меня, как человека, окончившего музыкальную школу когда-то, интересовало именно генерация мелодии на основе нот. Осталось только переложить знание музыкальной грамоты и физики на код.
Самым простым этапом в этом деле стало превратить ноту в код. По сути, любая нота характеризуется в первую очередь частотой звучания. Например, нота «ля» — это частота 440 Гц (стандартная частота камертона для настройки музыкальных инструментов).
По сути, нам остается только сопоставить каждой ноте её частоту. Всего нот (тонов) в октаве 7, а полутонов — 12. И у некоторых полутонов имеется несколько вариантов написания. Например, «фа-бемоль» это тоже самое, что и «ми». Или «соль-диез» это тоже самое, что и «ля-бемоль».
Итак, превратим эти знания в код:
class Note { const C = 261.63; const C_SHARP = 277.18; const D = 293.66; const D_FLAT = self::C_SHARP; const D_SHARP = 311.13; const E = 329.63; const E_FLAT = self::D_SHARP; const E_SHARP = self::F; const F = 346.23; const F_FLAT = self::E; const F_SHARP = 369.99; const G = 392.00; const G_FLAT = self::F_SHARP; const G_SHARP = 415.30; const A = 440.00; const A_FLAT = self::G_SHARP; const A_SHARP = 466.16; const H = 493.88; const H_FLAT = self::A_SHARP; public static function get($note) { switch ($note) { case 'C': return self::C; case 'C#': return self::C_SHARP; case 'D': return self::D; case 'D#': return self::D_SHARP; case 'E': return self::E; case 'E#': return self::E_SHARP; case 'F': return self::F; case 'F#': return self::F_SHARP; case 'G': return self::G; case 'G#': return self::G_SHARP; case 'A': return self::A; case 'A#': return self::A_SHARP; case 'B': return self::H_FLAT; case 'H': return self::H; } } }
Вообще, музыка достаточно точная наука. В нашем случае, это, в первую очередь, означает, что все возможные звучания различных инструментов уже давно описаны физиками и математиками, что, собственно, и позволяет производить, к примеру, синтезаторы. Подробно о синтезировании звуковых волн написано, например, здесь.
Ну а поскольку я еще и ленивый, подробно разбираться во всём этом деле у меня не было желания, поэтому я принялся яростно гуглить. Информацию об эмуляции звуков различных музыкальных инструментов на русском языке не нашлось ровным счетом ничего (может, конечно, я плохо искал, но не суть). Но в итоге мне удалось найти аудио-синтезатор, правда, на JavaScript (GitHub). В целом, оставалось только транслировать JS-код в PHP, чем я и занялся.
По итогу, получаем SampleBuilder, при помощи которого можем создавать сэмплы (куски wav-данных) задавая ноту, октаву и длительность звучания.
Код более подробно — по спойлером.
class Piano extends Generator { ... public function getDampen($sampleRate = null, $frequency = null, $volume = null) { return pow(0.5 * log(($frequency * $volume) / $sampleRate), 2); } ... public function getWave($sampleRate, $frequency, $volume, $i) { $base = $this->getModulations()[0]; return call_user_func_array($base, [ $i, $sampleRate, $frequency, pow(call_user_func_array($base, [$i, $sampleRate, $frequency, 0]), 2) + 0.75 * call_user_func_array($base, [$i, $sampleRate, $frequency, 0.25]) + 0.1 * call_user_func_array($base, [$i, $sampleRate, $frequency, 0.5]) ]); } ... protected function getModulations() { return [ function($i, $sampleRate, $frequency, $x) { return 1 * sin(2 * M_PI * (($i / $sampleRate) * $frequency) + $x); }, ... ]; } }
class SampleBuilder { /** * @var Generator */ protected $generator; ... public function note($note, $octave, $duration) { $result = new \SplFixedArray((int) ceil($this->getSampleRate() * $duration * 2)); $octave = min(8, max(1, $octave)); $frequency = Note::get($note) * pow(2, $octave - 4); $attack = $this->generator->getAttack($this->getSampleRate(), $frequency, $this->getVolume()); $dampen = $this->generator->getDampen($this->getSampleRate(), $frequency, $this->getVolume()); $attackLength = (int) ($this->getSampleRate() * $attack); $decayLength = (int) ($this->getSampleRate() * $duration); for ($i = 0; $i < $attackLength; $i++) { $value = $this->getVolume() * ($i / ($this->getSampleRate() * $attack)) * $this->getGenerator()->getWave( $this->getSampleRate(), $frequency, $this->getVolume(), $i ); $result[$i << 1] = Helper::packChar($value); $result[($i << 1) + 1] = Helper::packChar($value >> 8); } for (; $i < $decayLength; $i++) { $value = $this->getVolume() * pow((1 - (($i - ($this->getSampleRate() * $attack)) / ($this->getSampleRate() * ($duration - $attack)))), $dampen) * $this->getGenerator()->getWave( $this->getSampleRate(), $frequency, $this->getVolume(), $i ); $result[$i << 1] = Helper::packChar($value); $result[($i << 1) + 1] = Helper::packChar($value >> 8); } return new Sample($result->getSize(), implode('', $result->toArray())); } }
Ну и небольшой пример кода, который проигрывает начало всем известного «К Элизе» Л. Бетховена.
$sampleBuilder = new \Wav\SampleBuilder(\Wav\Generator\Piano::NAME); $samples = [ $sampleBuilder->note('E', 5, 0.3), $sampleBuilder->note('D#', 5, 0.3), $sampleBuilder->note('E', 5, 0.3), $sampleBuilder->note('D#', 5, 0.3), $sampleBuilder->note('E', 5, 0.3), $sampleBuilder->note('H', 4, 0.3), $sampleBuilder->note('D', 5, 0.3), $sampleBuilder->note('C', 5, 0.3), $sampleBuilder->note('A', 4, 1), ]; $builder = (new Wav\Builder()) ->setAudioFormat(\Wav\WaveFormat::PCM) ->setNumberOfChannels(1) ->setSampleRate(\Wav\Builder::DEFAULT_SAMPLE_RATE) ->setByteRate(\Wav\Builder::DEFAULT_SAMPLE_RATE * 1 * 16 / 8) ->setBlockAlign(1 * 16 / 8) ->setBitsPerSample(16) ->setSamples($samples); $audio = $builder->build(); $audio->returnContent();
Ссылки
Код полностью размещен на github: https://github.com/nkolosov/wav
Если кого-то заинтересовало, подключить к своему проекту можно при помощи composer:
composer require nkolosov/wav
Дальнейшие планы
Ну, во-первых, хотелось бы реализовать полную поддержку wav-файлов (обработку всех секций), реализовать поддержку многоканальных файлов, возможно — поддержку различных форматов wav (со сжатием и т.п), реализовать графическое отображение волны (на Хабре была статья о том, как это сделать на Python, мне же интересно сделать это на PHP).
В плане генерации, добавить еще некоторые инструменты, попытаться сделать звучание более плавным, чтобы получилась реальная возможность копировать целые музыкальные произведения в код, реализовать возможность проигрывать аккорды и т. д.
Если есть желающие присоединиться — welcome на GitHub.
ссылка на оригинал статьи https://habrahabr.ru/post/282922/
Добавить комментарий