📜 ⬆️ ⬇️

Work with WAV files using PHP

It all started with the fact that I thought about how to display on the site information about the downloaded audio file. First I decided to deal with the simplest format - wav. As it turned out, there is nothing difficult in this and writing about it, in general, there would be no point, good, information about how the wav file “from the inside” is full on the Internet.

And then Ostap suffered And then a bright thought came to mind that it would be fun not only to display information about the file, but also to be able to generate such a file on the fly. I think everyone has seen all sorts of “online pianos” on the net and stuff, right?

So, what I managed to do for 2 pm - under the cut.

So, to begin with, let us return to the structure of the WAV file, as such. For simplicity, we take the simplest single-channel wav file without compression.
')
Any wav file consists of several sections (chunks). In detail about all sections you can read, for example, by reference , I’ll focus on three main ones:


Each section has its ID, section size and, in fact, some data specific to this section.

Section RIFF is simple to disgrace: "RIFF <file size-8> WAVE"

<file size-8> because this value describes "how many bytes are contained further." Accordingly, 4 bytes for the value of "how much" and another 4 for the "RIFF" which was at the beginning.

The format section stores the main information of interest for the common person: Sample Rate (sampling frequency, for example, 44100 Hz), the number of channels (1 = mono, 2 = stereo, and so on).

In the data section, in fact, we need the necessary audio data for playing. In essence, they are the amplitude of a wave at a point in time.

Proceeding from the foregoing and proceeding from the specification of the format itself, nothing prevents us from writing the simplest classes describing each section we need and the simplest parser that will read the wav file and create the objects we need.

Header (Header.php)
class Header { ... /** * @var string */ protected $id; /** * @var int */ protected $size; /** * @var string */ protected $format; ... 


Format section (FormatSection.php)
 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; ... 


Data Section (DataSection.php)
 class DataSection { ... /** * @var string */ protected $id; /** * @var int */ protected $size; /** * @var int[] */ protected $raw; ... 


In the code above, all the logic is removed, we are now interested only in the structure of the data itself.

Actually, for their reading we will make a small wrapper-helper for fread for more convenient reading of binary data.

Helper.php
 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); } ... } 


It remains the case for small, take and read the contents of the wav file:

Reading data from a wav file
 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; } 


So, the data obtained, we can bring them in the right place by simply doing something in the spirit:

 echo $audio->getSampleRate(); 

Creating wav files


So, I, as a person who graduated from music school once, was interested in the generation of melodies based on notes. It remains only to shift the knowledge of musical literacy and physics to the code.

The simplest step in this business was to turn a note into a code. In fact, any note is characterized primarily by the frequency of the sound. For example, the note “la” is the frequency of 440 Hz (the standard frequency of the tuning fork for tuning musical instruments).

In fact, we can only compare each note to its frequency. Total notes (tones) in octave 7, and half tones - 12. And some half tones have several spellings. For example, "F-flat" is the same as "mi". Or G sharp is the same as A flat.

So, turn this knowledge into code:

Frequency constants for all notes
 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; } } } 


In general, music is quite an exact science. In our case, this, first of all, means that all possible sounds of various instruments have long been described by physicists and mathematicians, which, in fact, allows us to produce, for example, synthesizers. Details on the synthesis of sound waves are written, for example, here .

Well, since I am also lazy, I didn’t have a desire to understand the whole matter in detail, so I began to google furiously. Information about the emulation of the sounds of various musical instruments in Russian was not found absolutely nothing (maybe, of course, I was looking bad, but not the essence). But in the end I managed to find an audio synthesizer, though on JavaScript ( GitHub ). In general, it remained only to translate the JS-code in PHP, which I did.

As a result, we get SampleBuilder, with which we can create samples (pieces of wav-data) by specifying the note, octave and duration of the sound.

The code in more detail - by spoiler.

Samplebuilder
Piano sound generator
 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); }, ... ]; } } 


Samplebuilder
 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())); } } 



Well, and a small example of code that loses the beginning of the well-known “To Elise” L. Beethoven.

For Elise in PHP
 $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(); 


Links


The code is fully posted on github: https://github.com/nkolosov/wav

If someone is interested, you can connect to your project with the help of composer:

composer require nkolosov/wav

Future plans


Well, first of all, I would like to implement full support for wav-files (processing all sections), implement support for multi-channel files, perhaps support for various wav formats (with compression, etc.), implement a graphical wave display (on Habré there was an article about how to do it in Python , I'm also interested in doing it in PHP).

In terms of generation, add some more instruments, try to make the sound smoother, so that you can actually copy whole pieces of music into code, realize the ability to play chords, etc.

If you want to join - welcome to GitHub.

Source: https://habr.com/ru/post/282922/


All Articles