Back to Home

Work with WAV files using PHP

It all started with the fact that I thought about how to display information about the downloaded audio file on the site. To begin with · I decided to deal with the simplest format - wav. As it turned out · nothing complicated ...

Work with WAV files using PHP

It all started with the fact that I thought about how to display information about the downloaded audio file on the site. To begin with, I decided to deal with the simplest format - wav. As it turned out, there’s nothing complicated about it and writing about it, in general, wouldn’t make any sense, fortunately, the information on how the wav-file is built “from the inside” on the Internet is full.

And then Ostap suffered And then a bright thought came to my 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 saw all kinds of “online pianos” and stuff on the net, right?

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

So, for starters, let's 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, chunks). Details on all sections can be read, for example, by reference , but I will focus on three main ones:

  • type section -
    "RIFF"
  • format section -
    "fmt"
  • data section -
    "data"

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

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

<file size-8> because this value characterizes "how many bytes are contained next." Accordingly, 4 bytes on the value of "how much" and 4 more on the "RIFF" which was in the beginning.

In the format section, the main information about the file that interests the average person is stored: Sample Rate (sampling rate, for example 44100 Hz), number of channels (1 = mono, 2 = stereo, and so on).

In the data section, in fact, are the audio data we need to play. In fact, they are the amplitude of the wave at a time.

Based on the foregoing and the specifications of the format itself, nothing prevents us from writing the simplest classes that describe each section we need and the simplest parser that will read the wav file and create the objects we need.

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


Format Section (FormatSection.php)
classFormatSection{
    ...
    /**
     * @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)
classDataSection{
    ...
    /**
     * @var string
     */protected $id;
    /**
     * @var int
     */protected $size;
    /**
     * @var int[]
     */protected $raw;
    ...


All logic has been removed in the code above, we are only interested in the structure of the data itself.

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

Helper.php
classHelper{
    ...
    publicstaticfunctionreadString($handle, $length){
        returnself::readUnpacked($handle, 'a*', $length);
    }
    publicstaticfunctionreadLong($handle){
        returnself::readUnpacked($handle, 'V', 4);
    }
    publicstaticfunctionreadWord($handle){
        returnself::readUnpacked($handle, 'v', 2);
    }
    protectedfunctionreadUnpacked($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
classParser{
    ...
    publicstaticfunctionfromFile($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);
        }
        returnnew AudioFile($header, $formatSection, $dataSection);
    }
    protectedstaticfunctionparseHeader($handle){
        return [
            'id'     => Helper::readString($handle, 4),
            'size'   => Helper::readLong($handle),
            'format' => Helper::readString($handle, 4),
        ];
    }
    protectedstaticfunctionparseFormatSection($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),
        ];
    }
    protectedstaticfunctionparseDataSection($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 are received, we can deduce them in the right place by simple execution of 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 a code.

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

In fact, we can only match each note with its frequency. The total number of notes (tones) in the octave is 7, and half-tones - 12. And some half-tones have several spellings. For example, “F-flat” is the same as “E”. Or “G sharp” is the same as “A flat”.

So, we turn this knowledge into code:

Frequency constants for all notes
classNote{
    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;
    publicstaticfunctionget($note){
        switch ($note) {
            case'C':
                returnself::C;
            case'C#':
                returnself::C_SHARP;
            case'D':
                returnself::D;
            case'D#':
                returnself::D_SHARP;
            case'E':
                returnself::E;
            case'E#':
                returnself::E_SHARP;
            case'F':
                returnself::F;
            case'F#':
                returnself::F_SHARP;
            case'G':
                returnself::G;
            case'G#':
                returnself::G_SHARP;
            case'A':
                returnself::A;
            case'A#':
                returnself::A_SHARP;
            case'B':
                returnself::H_FLAT;
            case'H':
                returnself::H;
        }
    }
}


In general, music is a fairly accurate science. In our case, this primarily 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 about the synthesis of sound waves are written, for example, here .

Well, since I’m also lazy, I didn’t have a desire to understand all this business in detail, so I began to google fiercely. Information on emulating the sounds of various musical instruments in Russian was not found absolutely nothing (maybe, of course, I was looking badly, but not the point). But in the end, I managed to find an audio synthesizer, however, in JavaScript ( GitHub ). In general, it only remained to translate the JS code into PHP, which I did.

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

Code in more detail - by spoiler.

Samplebuilder
Piano sound generator
classPianoextendsGenerator{
    ...
    publicfunctiongetDampen($sampleRate = null, $frequency = null, $volume = null){
        return pow(0.5 * log(($frequency * $volume) / $sampleRate), 2);
    }
    ...
    publicfunctiongetWave($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])
        ]);
    }
    ...
    protectedfunctiongetModulations(){
        return [
            function($i, $sampleRate, $frequency, $x){
                return1 * sin(2 * M_PI * (($i / $sampleRate) * $frequency) + $x);
            },
            ...
        ];
    }
}


Samplebuilder
classSampleBuilder{
    /**
     * @var Generator
     */protected $generator;
    ...
    publicfunctionnote($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);
        }
        returnnew Sample($result->getSize(), implode('', $result->toArray()));
    }
}



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

To Eliza 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();


References


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

If anyone is interested, you can connect to your project using composer:

composer require nkolosov/wav

Future plans


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

In terms of generation, add some more instruments, try to make the sound smoother, so that you get a real opportunity to copy entire music to code, realize the ability to play chords, etc.

If you want to join, welcome to GitHub.

Read Next