recode.audio¶
Encoding audio
This module illustrates how one can use recode to make audio codecs.
>>> wav_bytes = encode_wav_bytes([1, 2, 3], 42)
>>> wav_bytes
b'RIFF*\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00*\x00\x00\x00T\x00\x00\x00\x02\x00\x10\x00data\x06\x00\x00\x00\x01\x00\x02\x00\x03\x00'
>>> wf, sr = decode_wav_bytes(wav_bytes)
>>> sr
42
>>> wf
[1, 2, 3]
The wav codecs are based on pcm codecs along with wav header codecs
(i.e. parsing and generation – using the builtin wave package).
Make pcm encoders and decoders:
>>> encode, decode = mk_pcm_audio_codec('int16')
>>> encoded = encode([1, 2, 3])
>>> encoded
b'\x01\x00\x02\x00\x03\x00'
>>> decode(encoded)
[1, 2, 3]
Or just encode directly:
>>> encode_pcm_bytes([1, 2, 3])
b'\x01\x00\x02\x00\x03\x00'
Or decode directly:
>>> encode_pcm_bytes([1, 2, 3])
b'\x01\x00\x02\x00\x03\x00'
Functions
|
|
|
Decode WAV bytes into a |
|
Get a dict of params decoded from a wav header |
|
Encode waveform (e.g. list of numbers) into PCM bytes. |
|
Encode waveform (e.g. list of numbers) into PCM bytes with WAV header. |
|
Make a WAV header from given parameters. |
|
Extract the header of a WAV file -- everything before the audio -- from its path. |
|
Size, in bytes, of everything preceding the audio payload. |
|
Make a (encoder, decoder) pair for PCM data with given width and n_channels. |
|
Find the target_num_sys equivalent of input num checking multiple unit options |
|
Translate from one (sample width) number type to another. |
Exceptions
The |
- exception recode.audio.ShortWavData[source]¶
Bases:
UserWarningThe
datachunk carries fewer bytes than its own header declares.Raised as a warning rather than an error because the audio that is present is still worth decoding – a partially downloaded file, or one written to a stream whose length was never patched back into the header. What must not happen is for the shortfall to pass unmentioned, since the caller cannot otherwise tell a truncated file from a complete one.
- recode.audio.decode_pcm_bytes(pcm_bytes, width=2, n_channels=1)[source]¶
- Parameters:
- Returns:
The decoded waveform
>>> decode_pcm_bytes(b'\x01\x00\x02\x00\x03\x00') [1, 2, 3]
- recode.audio.decode_wav_bytes(wav_bytes, *, eight_bit_unsigned=True)[source]¶
Decode WAV bytes into a
(waveform, sample_rate)pair.- Parameters:
wav_bytes (
bytes) – The bytes of a RIFF/WAVE container holding uncompressed PCMeight_bit_unsigned (
bool) – Read 8-bit audio as the unsigned PCM the WAV spec mandates (0..255 on disk, biased to -128..127 here). PassFalsefor the pre-recode#12 behaviour, which read those bytes as signed – silence came back as -128 – and so round-tripped withencode_wav_bytesbut with nothing else. Widths of 9 bits and up are signed in the spec and are unaffected either way.
- Returns:
(wf, sr)– the decoded waveform and its sample rate- Raises:
ValueError – if
wav_bytesis not a RIFF/WAVE container with adatachunk. (Before recode#4 the same inputs raisedAssertionError,wave.ErrororEOFErrordepending on how they were malformed; they are unified here.)ShortWavData – warning, not an exception – see below.
>>> wav_bytes = ( ... b'RIFF.\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00' # header ... b'*\x00\x00\x00T\x00\x00\x00\x02\x00\x10\x00data\n\x00\x00\x00' # header ... b'\x00\x00\x01\x00\xff\xff\x02\x00\xfe\xff' # data ... ) >>> wf, sr = decode_wav_bytes(wav_bytes) >>> wf [0, 1, -1, 2, -2] >>> sr 42
The
datachunk is located by walking the RIFF structure, so chunks that sit after the audio –LIST/INFOmetadata, which ffmpeg, Audacity and iTunes all append – do not shift the waveform:>>> import struct >>> info = b'INFOISFT' + struct.pack('<I', 6) + b'Lavf58' >>> with_trailing_metadata = wav_bytes + b'LIST' + struct.pack('<I', len(info)) + info >>> decode_wav_bytes(with_trailing_metadata)[0] [0, 1, -1, 2, -2]
A file carrying less audio than its header declares decodes to the whole frames that are actually there, and says so:
>>> truncated = wav_bytes[:-4] >>> import warnings >>> with warnings.catch_warnings(record=True) as caught: ... _ = warnings.simplefilter('always') ... wf, sr = decode_wav_bytes(truncated) >>> wf [0, 1, -1] >>> caught[0].category.__name__ 'ShortWavData'
8-bit WAV audio is stored unsigned, so a byte of 128 is silence rather than full negative (recode#12). Pass
eight_bit_unsigned=Falseto get the old signed reading:>>> eight_bit = encode_wav_bytes([-128, 0, 127], sr=42, width_bytes=1) >>> eight_bit[44:] b'\x00\x80\xff' >>> decode_wav_bytes(eight_bit)[0] [-128, 0, 127] >>> decode_wav_bytes(eight_bit, eight_bit_unsigned=False)[0] [0, -128, -1]
- recode.audio.decode_wav_header_bytes(wav_header_bytes)[source]¶
Get a dict of params decoded from a wav header
For examples, see the
encode_wav_header_bytesfunction, it’s inverse.- Return type:
>>> from recode.audio import encode_wav_header_bytes >>> header_bytes = encode_wav_header_bytes(44100, 2, n_channels=3) >>> decode_wav_header_bytes(header_bytes) {'sr': 44100, 'width_bytes': 2, 'n_channels': 3, 'nframes': 0, 'comptype': None}
Stdlib
waveis the primary reader. AWAVE_FORMAT_EXTENSIBLEheader, which it refuses before Python 3.12, is parsed directly instead so the same file decodes on every supported version – see_extensible_wav_header_params().
- recode.audio.encode_pcm_bytes(wf, width=16, n_channels=1)[source]¶
Encode waveform (e.g. list of numbers) into PCM bytes.
- Parameters:
- Returns:
The pcm-bytes-encoded waveform
>>> encode_pcm_bytes([1, 2, 3]) b'\x01\x00\x02\x00\x03\x00'
- recode.audio.encode_wav_bytes(wf, sr, width_bytes=2, n_channels=1, *, eight_bit_unsigned=True)[source]¶
Encode waveform (e.g. list of numbers) into PCM bytes with WAV header.
- Parameters:
wf (
Iterable[Number]) – Waveform to encode (iterable of numbers)sr (
int) – Sample rate in Hzwidth_bytes (
int) – The width of a sample in bytesn_channels (
int) – Number of channelseight_bit_unsigned (
bool) – Write 8-bit audio as the unsigned PCM the WAV spec mandates (samples biased by 128 into 0..255 on disk). PassFalsefor the pre-recode#12 behaviour, which wrote them signed – files that recode read back correctly and every other tool did not. Widths of 9 bits and up are signed in the spec and are unaffected either way.
- Returns:
The complete WAV file bytes (header + data)
- Return type:
Examples
>>> wav_bytes = encode_wav_bytes([0, 1, -1, 2, -2], sr=42) >>> header_bytes, data_bytes = wav_bytes[:44], wav_bytes[44:] >>> data_bytes b'\x00\x00\x01\x00\xff\xff\x02\x00\xfe\xff'
See that the header bytes can be decoded to get the right information about our waveform:
>>> decode_wav_header_bytes(header_bytes) {'sr': 42, 'width_bytes': 2, 'n_channels': 1, 'nframes': 5, 'comptype': None}
See that our wave_bytes can be decoded to get the original waveform and sample rate:
>>> decoded_wf, decoded_sr = decode_wav_bytes(wav_bytes) >>> decoded_wf [0, 1, -1, 2, -2] >>> decoded_sr 42
- recode.audio.encode_wav_header_bytes(sr, width_bytes, *, n_channels=1, nframes=0, comptype=None)[source]¶
Make a WAV header from given parameters.
- Parameters:
sr (
int) – The sample rate (i.e. “frame rate” i.e. “chk_rate”)width_bytes (
int) – The “sample width” in bytesn_channels (
int) – Number of channels (default is 1)nframes (
int) –Optional number of frames (default is 0).
- NOTE:
If a wav file is to be read correctly, the num of frames (i.e. samples/chks) should be exactly the number you’ll actually be writing in the wave file.
comptype – No supported by python’s wave module (yet).
- Return type:
>>> header_bytes = encode_wav_header_bytes(44100, 2, n_channels=3) >>> len(header_bytes) 44 >>> header_bytes[:31] b'RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x03\x00D\xac\x00\x00\x98\t\x04'
You can decode those params (including those you didn’t specify, but were defaulted) with the
decode_wav_header_bytesinverse function.>>> from recode.audio import decode_wav_header_bytes >>> params = decode_wav_header_bytes(header_bytes) >>> params {'sr': 44100, 'width_bytes': 2, 'n_channels': 3, 'nframes': 0, 'comptype': None} >>> assert encode_wav_header_bytes(**params) == header_bytes
- recode.audio.extract_wav_header_from_file(filepath, *, read_size=65536)[source]¶
Extract the header of a WAV file – everything before the audio – from its path.
Useful for reading the header of a WAV file without having to read the entire file into memory, which is what you want when WAV files are large and/or numerous: only as much of the file as the header occupies is read.
The answer is the same one
header_size_of_wav_bytes()gives for the same bytes, because both locate thedatachunk by walking the RIFF structure rather than inferring where it must be. This used to computechunk_size + 8 - subchunk2_size, reading bytes 40-44 as the size of the audio – true only of a bare 44-byte header. With aLIST/INFOchunk after the audio (what ffmpeg, Audacity and iTunes write) it returned audio bytes as header; with one before, a truncated prefix that parses as nothing at all (recode#4).- Parameters:
filepath – The path to the WAV file
read_size (
int) – How many bytes to read at a time while looking for the audio
- Returns:
The bytes of the WAV file header, i.e. everything preceding the
datachunk’s contents- Raises:
ValueError – if the file is not a RIFF/WAVE container with a reachable
datachunk. (It used to answer such files with arbitrary bytes and no complaint.)
- recode.audio.header_size_of_wav_bytes(wav_bytes, meta=None)[source]¶
Size, in bytes, of everything preceding the audio payload.
That is the offset of the
datachunk’s contents, found by walking the RIFF structure (see_wav_data_chunk()). For a well-formed file with nothing after the audio this is the same number the old size-subtraction produced; unlike it, it stays correct when the file carries trailing metadata or an over-declareddatasize.meta– an already-decoded header, once passed to save re-parsing it – is accepted for backwards compatibility and no longer used.- Return type:
>>> header_size_of_wav_bytes( ... b'RIFF.\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00' ... b'*\x00\x00\x00T\x00\x00\x00\x02\x00\x10\x00data\n\x00\x00\x00' ... b'\x00\x00\x01\x00\xff\xff\x02\x00\xfe\xff' ... ) 44
- recode.audio.mk_pcm_audio_codec(width=16, n_channels=1)[source]¶
Make a (encoder, decoder) pair for PCM data with given width and n_channels.
PCM data is what’s used in the uncompressed raw WAVE formats (such as used in CDs). See https://en.wikipedia.org/wiki/Pulse-code_modulation.
- Parameters:
- Returns:
A (encoder, decoder) pair of functions that are inverse of each other
>>> encode, decode = mk_pcm_audio_codec('int16') >>> encoded = encode([1, 2, 3]) >>> encoded b'\x01\x00\x02\x00\x03\x00' >>> decode(encoded) [1, 2, 3]
Let’s check over more combinations of width and n_channels that we can decode what we encode to get back the same thing:
>>> wf = [-3, -2, -1, 0, 1, 2, 3] >>> for width in [16, 2, 'int16', 'paInt16', 'PCM_16', 32, 4, 'int32']: ... for channel in wf: ... encode, decode = mk_pcm_audio_codec('int16') ... encoded = encode(wf) ... assert isinstance(encoded, bytes) ... assert decode(encoded) == wf
- recode.audio.num_find_num_type_for(num, target_num_sys='struct', num_sys_search_order=('n_bits', 'n_bytes', 'dtype', 'pyaudio', 'soundfile'))[source]¶
Find the target_num_sys equivalent of input num checking multiple unit options
- recode.audio.num_type_for(num, num_sys='n_bits', target_num_sys='struct')[source]¶
Translate from one (sample width) number type to another.
- Parameters:
num
num_sys
target_num_sys
- Returns:
>>> num_type_for(16, "n_bits", "soundfile") 'PCM_16' >>> num_type_for(3, "n_bytes", "soundfile") 'PCM_24'
Tip
Use with
functools.partialwhen you have some fix translation endpoints.>>> from functools import partial >>> get_dtype_from_n_bytes = partial( ... num_type_for, num_sys="n_bytes", target_num_sys="dtype" ... ) >>> get_dtype_from_n_bytes(8) 'float64'