Acoustic signal processing with Python (2)

This article is the 17th day article of Python Advent Calendar. I will write about the contents related to acoustic signal processing using Python that I did not write in Last year's article.

table of contents

I want to convert the sampling rate

In order to convert the sampling rate, if the original sampling rate and the sampling rate you want to convert are integer ratios, you only need to use either up / down sampling, but in the case of rational ratios, use the least common multiple of each sampling rate. You need to upsample and then downsample to the desired sampling rate.

Here we consider converting the sampling rate from 48kHz to 44.1kHz. Sampling rate conversion can be realized with the following code. The target is the first 10 seconds of a 48kHz sound source.

#!/usr/bin/env python
# vim:fileencoding=utf-8

from fractions import Fraction

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt

import scipy.signal as sg

import soundfile as sf

if __name__ == '__main__':

    fs_target = 44100
    cutoff_hz = 21000.0
    n_lpf = 4096

    sec = 10

    wav, fs_src ="../wav/souvenir_mono_16bit_48kHz.wav")
    wav_48kHz = wav[:fs_src * sec]

    frac = Fraction(fs_target, fs_src)  # 44100 / 48000

    up = frac.numerator  # 147
    down = frac.denominator  # 160

    # up sampling
    wav_up = np.zeros(np.alen(wav_48kHz) * up)
    wav_up[::up] = up * wav_48kHz
    fs_up = fs_src * up

    cutoff = cutoff_hz / (fs_up / 2.0)
    lpf = sg.firwin(n_lpf, cutoff)

    # filtering and down sampling
    wav_down = sg.lfilter(lpf, [1], wav_up)[n_lpf // 2::down]

    # write wave file
    sf.write("down.wav", wav_down, fs_target)

    # lowpass filter plot
    w, h = sg.freqz(lpf, a=1, worN=1024)
    f = fs_up * w / (2.0 * np.pi)
    fig = plt.figure(1)
    ax = fig.add_subplot(111)
    ax.semilogx(f, 20.0 * np.log10(np.abs(h)))
    ax.axvline(fs_target, color="r")
    ax.set_ylim([-80.0, 10.0])
    ax.set_xlim([3000.0, fs_target + 5000.0])
    ax.set_xlabel("frequency [Hz]")
    ax.set_ylabel("power [dB]")


Since 44100/48000 = 147/160, the original sound source is first upsampled to 147 times the score. Here, a buffer with a size of 147 times is created, and the original signal is substituted into the buffer at intervals of 147 points. As a result, the points that have not been assigned are zero-padded. Then design the LPF to include only frequencies below the Nyquist frequency of 44100Hz and use the lfilter to apply the LPF to limit the bandwidth. The point here is that the Nyquist frequency during upsampling is 48000/2 = 24000Hz, but after downsampling, 44100/2 = 22050Hz is the Nyquist frequency, so the band limiting filter applied during upsampling is 22050 or less. It is possible to use only one filter that becomes. After limiting the band, after correcting the delay due to the LPF, downsampling is performed at intervals of 160 points. As a result, sampling rate conversion was realized.

The actual audio waveform and spectrogram are as follows. The top two are after 44.1kHz conversion and the bottom two are before conversion.


I want to read a 24-bit audio file with the wave module

If you want to read a 16-bit audio file with a standard wave module, you can read it with and read as much as you need with readframes and convert it to int16 with np.frombuffer, but in the case of a 24-bit sound source, you cannot specify 24bit with frombuffer. Therefore, you need to read it by yourself. Here, as shown in the code below, while reading 3 bytes at a time using the unpack of the struct module, the 24-bit sound source is read by packing 0s and unpacking as int32.

By the way, the sound source being read is e-onkyo sample sound source.

#!/usr/bin/env python
# vim:fileencoding=utf-8

import numpy as np
import matplotlib.pyplot as plt
import wave

from struct import unpack

if __name__ == '__main__':

    fname = "../wav/souvenir.wav"
    fp =, "r")

    nframe = fp.getnframes()
    nchan = fp.getnchannels()
    nbyte = fp.getsampwidth()
    fs = fp.getframerate()

    print("frame:{0}, "
          "channel:{1}, "
          "bytewidth:{2}, "
          "fs:{3}".format(nframe, nchan, nbyte, fs))

    buf = fp.readframes(nframe * nchan)

    read_sec = 40
    read_sample = read_sec * nchan * fs
    print("read {0} second (= {1} frame)...".format(read_sec,

    #By packing 0 in the lowest bit and unpacking int
    #Extract the value with a 24-bit value as a 32-bit int
    #  (<i assumes little endian int value)
    #unpack returns tuple[0]I take the
    unpacked_buf = [unpack("<i",
                           bytearray([0]) + buf[nbyte * i:nbyte * (i + 1)])[0]
                    for i in range(read_sample)]

    ndarr_buf = np.array(unpacked_buf)

    # -1.0〜1.Normalize to 0
    float_buf = np.where(ndarr_buf > 0,
                         ndarr_buf / (2.0 ** 31 - 1),
                         ndarr_buf / (2.0 ** 31))

    #Solve interleave(For stereo sound source)
    wav_l = float_buf[::2]
    wav_r = float_buf[1::2]
    time = np.arange(np.alen(wav_l)) / fs

    # plot
    fig = plt.figure(1)

    ax = fig.add_subplot(2, 1, 1)
    ax.plot(time, wav_l)
    ax.set_xlabel("time [pt]")
    ax.set_title("left channel")

    ax = fig.add_subplot(2, 1, 2)
    ax.plot(time, wav_r)
    ax.set_xlabel("time [pt]")
    ax.set_title("right channel")


I want to estimate the group delay with scipy.signal.groupdelay

[This item] from last year's article ( A8% 88% E7% AE% 97% E3% 81% 97% E3% 81% 9F% E3% 81% 84) I didn't notice when I wrote it, but I didn't notice it, but the [group_delay method] for finding the group delay ( has been added since version 0.16.0 of scipy.signal. It was. This can be used to determine the group delay of a digital filter.

#!/usr/bin/env python
# vim:fileencoding=utf-8

import numpy as np
import matplotlib.pyplot as plt

import scipy.signal as sg

def allpass_filter(freq, r=0.9, fs=16000.0):
    omega = 2.0 * np.pi * freq / fs

    b = [r ** 2, - 2.0 * r * np.cos(omega), 1.0]
    a = [1.0, -2.0 * r * np.cos(omega), r ** 2]

    return (b, a)

if __name__ == '__main__':

    fs = 16000.0
    N = 1024

    b, a = allpass_filter(1000.0)
    w, h = sg.freqz(b, a, worN=N)

    f = w * fs / (2.0 * np.pi)
    _, gd = sg.group_delay((b, a), w=w)

    fig = plt.figure(1)
    ax = fig.add_subplot(3, 1, 1)
    ax.semilogx(f, np.abs(h))
    ax.set_xlabel("frequency [Hz]")
    ax.set_xlim([10.0, fs / 2.0])

    ax = fig.add_subplot(3, 1, 2)
    ax.semilogx(f, np.angle(h))
    ax.set_xlabel("frequency [Hz]")
    ax.set_ylabel("phase [rad]")
    ax.set_xlim([10.0, fs / 2.0])

    ax = fig.add_subplot(3, 1, 3)
    ax.semilogx(f, gd)
    ax.set_xlabel("frequency [Hz]")
    ax.set_ylabel("group delay [pt]")
    ax.set_xlim([10.0, fs / 2.0])


I want to do frame processing using numpy's stride trick (I want to draw a spectrogram)

Using the stride trick introduced in recipes 4.6 and 4.7 of IPython Data Science Cookbook, one buffer is divided into short frames. You can efficiently perform frame processing for repeated access without generating a copy of the frame. Using as_strided under numpy.lib.stride_tricks, you can write the method for calculating the spectrogram as follows.

#!/usr/bin/env python

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
import as cm

import scipy.fftpack as fft
from numpy.lib.stride_tricks import as_strided

import soundfile as sf

def spectrogram(xs, nfft=1024, noverlap=2, win=np.hanning):

    nsample = np.alen(xs)
    nframe = int(nsample * (noverlap / float(nfft))) - noverlap
    nbyte = xs.dtype.itemsize
    shift = (nfft // noverlap)

    xs_split = as_strided(xs, (nframe, nfft), (nbyte * shift, nbyte))
    window = win(nfft)

    Xs = []
    print(nsample, nframe, nfft, shift, nbyte, nfft * nframe)
    for frame in xs_split:
        Xs.append(20.0 * np.log10(np.abs(fft.fft(window * frame, nfft))[:nfft//2 + 1]))

    return np.array(Xs)

if __name__ == '__main__':

    wav, fs ="../wav/souvenir_mono_16bit_48kHz.wav")

    sec = 20
    nread = fs * sec

    nfft = 1024
    spec = spectrogram(wav[:nread], nfft=nfft, noverlap=2)

    # plot
    fig = plt.figure(1)
    ax = fig.add_subplot(211)

    ax = fig.add_subplot(212)
              np.arange(nfft//2 + 1) * (fs / nfft),
    ax.set_xlim([0, spec.shape[0]])
    ax.set_ylim([50, fs//2])


stride is a value that represents the byte interval when accessing a contiguous area. If you change this, you will be able to control how many bytes you go ahead when you advance one row or column index.

as_strided is a method to access the buffer with this stride changed in a pseudo manner. The first argument is the access destination buffer, the second argument is the shape of the buffer created by changing the stride, and the third argument is the stride you want to change. If you access after changing the buffer of the first argument to the stride of the third argument in a pseudo manner, the operation will be such that an alias that can be accessed as the buffer indicated by the shape of the second argument is returned.

The format of this stride is (byte spacing when advancing rows, byte spacing when advancing columns). In the case of the above script, the original argument xs is a one-dimensional array and its stride is (8,), so if you advance the index by 1, it will advance by 8 bytes (dtype is float64). If this stride is changed to (512 * 8, 8) by, for example, as_stride, xs, which was originally a one-dimensional array, can be regarded as a two-dimensional array xs_split. If you repeatedly access xs_split with a for loop, each time you access it, it will go in the row direction of xs_split, that is, it will go 512 * 8 bytes ahead of xs, so you can access by shifting xs by 512pt. In this way, frame processing that would otherwise be processed by cutting out a one-dimensional array into multiple short-time frames can now be performed by stride tricks without causing copying at the time of cutting out. I will.

Note that this stride trick makes it easy to access outside the range of the internal buffer, so if you make a mistake, you will often access the undefined area and fall.

I want to generate a TSP signal

When designing a Sweep Pulse (TSP signal) used for acoustic measurement, I think that it is often obtained by IFFTing what was designed in the frequency domain and returning it to the time domain. It can be generated with the following code. N_exp and m_exp, which contributes to the effective length, need adjustment.

#!/usr/bin/env python
# vim:fileencoding=utf-8

import numpy as np
import matplotlib.pyplot as plt

import scipy.fftpack as fft

import soundfile as sf

if __name__ == '__main__':

    # parameters
    N_exp = 16
    m_exp = 2
    nrepeat = 5
    fs = 48000
    gain = 100.0

    N = 2 ** N_exp
    m = N // (2 ** m_exp)  # (J=2m)
    a = ((m * np.pi) * (2.0 / N) ** 2)

    tsp_freqs = np.zeros(N, dtype=np.complex128)
    tsp_freqs[:(N // 2) + 1] = np.exp(-1j * a * (np.arange((N // 2) + 1) ** 2))
    tsp_freqs[(N // 2) + 1:] = np.conj(tsp_freqs[1:(N // 2)][::-1])

    # ifft and real
    tsp = np.real(fft.ifft(tsp_freqs, N))

    # roll
    tsp = gain * np.roll(tsp, (N // 2) - m)

    # repeat
    tsp_repeat = np.r_[np.tile(tsp, nrepeat), np.zeros(N)]

    # write
    sf.write("tsp.wav", tsp_repeat, fs)

    fig = plt.figure(1)
    ax = fig.add_subplot(211)
    ax = fig.add_subplot(212)

If you display the wav file generated by this with audacity, it will be as follows.


The TSP signal is described in detail in Professor Kaneda's basic training materials on impulse response measurement below.

I want to visualize zeros and poles

You can convert the time filter coefficient to zeros and poles with tf2zpk, but to visualize this, you can draw a circle using add_patch as shown below, and then plot without lines and with markers. .. I noticed after pasting the figure, but since it is an FIR filter, there is no pole, but please ignore it.

#!/usr/bin/env python

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt

import scipy.signal as sg

if __name__ == '__main__':

    b = sg.firwin(11, 0.2, window="han")
    z, p, k = sg.tf2zpk(b, [1])

    fig = plt.figure(1, figsize=(8, 8))
    ax = fig.add_subplot(111)

    ax.add_patch(plt.Circle((0.0, 0.0), 1.0, fc="white"))
    ax.plot(np.real(z), np.imag(z), "o", mfc="white")
    ax.plot(np.real(p), np.imag(p), "x", mfc="white")

    ax.set_xlim([-1.5, 1.5])
    ax.set_ylim([-1.5, 1.5])



I also wanted to write the following content, but I didn't have enough time, so I'd like to add it again or write it in another article.

Regarding the Hilbert transform, I wrote only the following explanation, so I will post it for the time being.

In order to find the envelope that smoothly traces the maximum amplitude of the signal, there is a method of multiplying the signal with the absolute value or square value by LPF, but also the method of finding it from the analysis signal obtained by the Hilbert transform. Has been done.


Recommended Posts

Acoustic signal processing with Python (2)
Acoustic signal processing with Python
Image processing with Python
Image processing with Python (Part 2)
100 Language Processing with Python Knock 2015
"Apple processing" with OpenCV3 + Python3
Image processing with Python (Part 1)
Image processing with Python (Part 3)
[Python] Image processing with scikit-image
High resolution acoustic signal processing (1) --How to read 24-bit wav file with Python
Signal processing in Python (1): Fourier transform
100 Language Processing Knock with Python (Chapter 1)
Try audio signal processing with librosa-Beginner
100 Language Processing Knock with Python (Chapter 3)
Image processing with Python 100 knocks # 3 Binarization
Image processing with Python 100 knocks # 2 Grayscale
Acoustic signal processing starting with Python-Let's make a stereophonic system
Basics of binarized image processing with Python
Image processing with Python 100 knock # 10 median filter
FizzBuzz with Python3
Scraping with Python
python image processing
Statistics with python
Scraping with Python
Python with Go
Periodically perform arbitrary processing with Python Twisted
Twilio with Python
Let Heroku do background processing with Python
Integrate with Python
Play with 2016-Python
100 Language Processing Knock with Python (Chapter 2, Part 2)
Python file processing
AES256 with python
Image processing with Python & OpenCV [Tone Curve]
Tested with Python
3. Natural language processing with Python 2-1. Co-occurrence network
Image processing with Python 100 knock # 12 motion filter
python starts with ()
3. Natural language processing with Python 1-1. Word N-gram
[Python / PyRoom Acoustics] Room acoustic simulation with Python
100 Language Processing Knock with Python (Chapter 2, Part 1)
with syntax (Python)
Bingo with python
Drawing with Matrix-Reinventor of Python Image Processing-
Zundokokiyoshi with python
Easy image processing in Python with Pillow
Image processing with Python 100 knocks # 7 Average pooling
Light image processing with Python x OpenCV
Excel with Python
Image processing with Python 100 knocks # 9 Gaussian filter
Microcomputer with Python
Cast with python
Acoustic signal processing module that can be used with Python-Sounddevice ASIO [Application]
Acoustic signal processing module that can be used with Python-Sounddevice ASIO [Basic]
Getting started with Python with 100 knocks on language processing
How to do multi-core parallel processing with python
Operate the Speech Signal Processing Toolkit via python
Image processing from scratch with python (5) Fourier transform
[Python] I played with natural language processing ~ transformers ~
Image processing from scratch with python (4) Contour extraction
Image Processing with Python Environment Setup for Windows