Methods of compressing/storing media data in WAVE and JPEG formats, part 1

Hello! My first series of articles will focus on exploring methods for compressing and storing images/sound, such as JPEG (image) and WAVE (audio), with practical examples using these formats (.jpg, .wav). In this part, we will specifically look at WAVE.

History

WAVE (Waveform Audio File Format) is a container file format for storing audio stream recordings. This container is typically used to store uncompressed sound in pulse-code modulation. (Taken from Wikipedia)

It was invented and published in 1991 along with RIFF by Microsoft and IBM (the leading IT companies of that time).

File Structure

The file has a header section, the actual data, but no footer. The header weighs a total of 44 bytes.
The header contains settings for bits per sample, sample rate, bit depth, and other information necessary for the sound card. (All numerical values in the table must be recorded in Little-Endian order)

Block Name
Block Size (B)
Description/Purpose
Value (some are fixed)

chunkId
4
Defines the file as a media container
0x52494646 in Big-Endian ("RIFF")

chunkSize
4
Size of the entire file without chunkId and chunkSize
FILE_SIZE — 8

format
4
Defines the type from RIFF
0x57415645 in Big-Endian ("WAVE")

subchunk1Id
4
To make the file take up more space, continuing the format
0x666d7420 in Big-Endian ("fmt ")

subchunk1Size
4
Remaining header (in bytes)
16 by default (for uncompressed audio stream case)

audioFormat
2
Audio format (depends on the compression method and structure of audio data)
1 (for PCM, which we are considering)

numChannels
2
Number of channels
1/2, we will take 1 channel (3/4/5/6/7... — specific audio track, for example 4 for quad sound, etc.)

sampleRate
4
Sampling frequency of sound (in Hertz)
The higher the value, the better quality the sound will be, but more memory will be needed to create an audio track of the same length; the recommended value is 48000 (optimal sound quality)

byteRate
4
Number of bytes per second
sampleRate numChannels bitsPerSample (below)

blockAlign
2
Number of bytes for 1 sample
numChannels * bitsPerSample: 8

bitsPerSample
2
Number of bits per sample (depth)
Any number that is a multiple of 8. The higher, the better and heavier the audio will be; from 32 bits, there is no difference for human hearing.

subchunk2Id
4
Marker for the start of data (as there can be other header elements depending on audioFormat)
0x64617461 in Big-Endian ("data")

subchunk2Size
4
Size of the data area
size of data in int

data
byteRate * duration of audio
Audio Data
?

Example with WAVE

The previous table can easily be translated into a structure in C, but our language today is Python. The simplest thing we can do using 'wave' is to generate noise. For this task, we won't require high byteRate or compression.
First, let's import the necessary modules:

# WAV.py

from struct import pack  # перевод py-объектов в базовые типы из C
from os import urandom  # функция для чтения /dev/urandom, для windows:
# from random import randint
# urandom = lambda sz: bytes([randint(0, 255) for _ in range(sz)])  # лямбда под windows, т.к. urandom'а в винде нет
from sys import argv, exit  # аргументы к проге и выход

if len(argv) != 3:  # +1 имя скрипта (-1, если будете замораживать)
    print('Usage: python3 WAV.py [num of samples] [output]')
    exit(1)

Next, we need to create all the necessary variables from the table based on their sizes. The variable sizes here depend only on numSamples (the number of samples). The more samples there are, the longer our noise will run.

numSamples = int(argv[1])
output_path = argv[2]

chunkId = b'RIFF'
Format = b'WAVE'
subchunk1ID = b'fmt '
subchunk1Size = b'x10x00x00x00'  # 0d16
audioFormat = b'x01x00'
numChannels = b'x02x00'  # 2 channels will be enough (stereo)
sampleRate = pack('<L', 1000)  # 1000 is sufficient, but if you increase it, the noise will be clearer. At 1000, it sounds like wind
bitsPerSample = b'x20x00'  # 0d32
byteRate = pack('<L', 1000 * 2 * 4)  # sampleRate * numChannels * bitsPerSample / 8  (32 bit sound)
blockAlign = b'x08x00'  # numChannels * BPS / 8
subchunk2ID = b'data'
subchunk2Size = pack('<L', numSamples * 2 * 4)  # * numChannels * BPS / 8
chunkSize = pack('<L', 36 + numSamples * 2 * 4)  # 36 + subchunk2Size

data = urandom(1000 * 2 * 4 * numSamples)  # the noise itself

All that's left is to write them in the required order (as in the table):

with open(output_path, 'wb') as fh:
    fh.write(chunkId + chunkSize + Format + subchunk1ID +
            subchunk1Size + audioFormat + numChannels + 
            sampleRate + byteRate + blockAlign + bitsPerSample +
            subchunk2ID + subchunk2Size + data)  # writing it out

And that's it. To use the script, we need to add the required command line arguments:
python3 WAV.py [num of samples] [output]
num of samples — number of samples
output — path to the output file

Here's a link to a test audio file with noise, but to save memory, I reduced the BPS to 1b/s and the number of channels to 1 (with a 32-bit uncompressed stereo audio stream at 64kbs, it resulted in an 80M clean .wav file, but now it's only 10): https://instaud.io/3Dcy

The full code (WAV.py) (The code has many duplicate variable values, this is just a draft):

from struct import pack  # translating py-objects to basic types from C
from os import urandom  # function for reading /dev/urandom, for windows:
# from random import randint
# urandom = lambda sz: bytes([randint(0, 255) for _ in range(sz)])  # lambda for windows, since there's no urandom in Windows
from sys import argv, exit  # program arguments and exit

if len(argv) != 3:  # +1 for script name (-1 if you will be freezing)
    print('Usage: python3 WAV.py [num of samples] [output]')
    exit(1)

numSamples = int(argv[1])
output_path = argv[2]

chunkId = b'RIFF'
Format = b'WAVE'
subchunk1ID = b'fmt '
subchunk1Size = b'\x10\x00\x00\x00'  # 0d16
audioFormat = b'\x01\x00'
numChannels = b'\x02\x00'  # 2 channels will be enough (stereo) 
sampleRate = pack('<L', 1000)  # 1000 will suffice, but more is possible.
bitsPerSample = b'\x20\x00'  # 0d32
byteRate = pack('<L', 1000 * 2 * 4)  # sampleRate * numChannels * bitsPerSample / 8  (32 bit sound)
blockAlign = b'\x08\x00'  # numChannels * BPS / 8
subchunk2ID = b'data'
subchunk2Size = pack('<L', numSamples * 2 * 4)  # * numChannels * BPS / 8
chunkSize = pack('<L', 36 + numSamples * 2 * 4)  # 36 + subchunk2Size

data = urandom(1000 * 2 * 4 * numSamples)  # the noise itself

with open(output_path, 'wb') as fh:
    fh.write(chunkId + chunkSize + Format + subchunk1ID +
            subchunk1Size + audioFormat + numChannels + 
            sampleRate + byteRate + blockAlign + bitsPerSample +
            subchunk2ID + subchunk2Size + data)  # writing the result to the file

Summary

Now you've learned a bit more about digital sound and how it's stored. In this post, we didn't use compression (audioFormat), but discussing each of the popular ones would require about 10 articles. I hope you learned something new that will help you in future developments.
Thank you!

file — continuous reading of events from one or more local files;

WAV File Structure
WAV — Wikipedia

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster