Work with WAV files using PHP
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.
classHeader{
...
/**
* @var string
*/protected $id;
/**
* @var int
*/protected $size;
/**
* @var string
*/protected $format;
...
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;
...
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.
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:
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:
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.
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);
},
...
];
}
}
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”.
$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.