การอ่านค่าสัญญาณเสียงด้วย Python และการแสดงรูปสเปกตรัมเชิงความถี่#
บทความนี้กล่าวถึง การเขียนโค้ด Python เพื่ออ่านค่าสัญญาณเสียงจากไมโครโฟน และวิเคราะห์ข้อมูลด้วย FFT (Fast-Fourier Transform) สำหรับการแสดงผลในรูปของสเปกตรัมเชิงความถี่
Keywords: Python, PyAudio, NumPy / Matplotlib
▷ FFT (Fast-Fourier Transform)#
FFT เป็นอัลกอริทึมที่มีประสิทธิสูงสำหรับการคำนวณทางคณิตศาสตร์ตามรูปแบบที่เรียกว่า Discrete Fourier Transform (DFT) ซึ่งเป็นหนึ่งในวิธีการวิเคราะห์สัญญาณของฟูเรียร์ (Fourier Analysis)
อินพุตเป็นชุดข้อมูลจำนวนจำกัดที่ได้จาก "การชักตัวอย่าง" หรือ "การสุ่มตัวอย่าง" (Sampling) ของสัญญาณที่มีการเปลี่ยนแปลงค่าในเชิงเวลา และได้เอาต์พุตเป็นชุดของค่าสัมประสิทธิ์ที่เป็นเลขเชิงซ้อน (DFT Coefficients) ที่มีจำนวนเท่ากันกับข้อมูลอินพุต และนำไปวิเคราะห์คุณสมบัติของสัญญาณในเชิงความถี่ เช่น การแสดงผลในรูปแบบที่เรียกว่า "สเปกตรัมเชิงความถี่" (Frequency Spectrum) ซึ่งแสดงองค์ประกอบของสัญญาณในโดเมนความถี่
FFT ถูกจัดอยู่ในกลุ่มที่เรียกว่า "การประมวลผลสัญญาณดิจิทัล" (DSP: Digital Signal Processing) และใช้สำหรับการวิเคราะห์ชุดข้อมูลหรือสัญญาณดิจิทัลในเชิงความถี่
ถ้าจะประมวลผลข้อมูลด้วยการเขียนโปรแกรมในภาษา Python ก็สามารถทำได้ง่าย เนื่องจากมีไลบรารี หรือ แพ็กเกจ (Python Packages) เช่น NumPy ไว้ให้ใช้งาน
ลองมาดูตัวอย่างโค้ด Python ที่มีการใช้งาน NumPy และ Matplotlib เพื่อการวิเคราะห์ข้อมูลสังเคราะห์ที่ได้จากการชักตัวอย่าง ด้วยอัตราคงที่ จากสัญญาณรูปคลื่นไซน์ (Sinusoidal Wave) ที่มีความถี่เท่ากับ 500Hz และมีสัญญาณรบกวน (Noise) รวมอยู่ด้วย
import numpy as np
import matplotlib.pyplot as plt
N = 1024 # Number of samples
Fs = 8000 # Sampling frequency in Hz
f = 500 # The frequency of the sine wave as a test signal.
A = 10000 # The amplitude of the sine wave (=> 16-bit signed).
ts = np.arange(N)/Fs # time values
noise = (10*np.random.uniform(-1, 1, size=N)).astype(np.int16)
data = (A*np.sin(2*np.pi*f*ts)).astype(np.float32)
data += noise # data plus noise
# Apply a window function (Hanning window in this case)
window_coeffs = np.hanning(N).astype( np.float32 )
windowed_data = data * window_coeffs
# Compute the FFT using NumPy
fft_result = np.fft.fft( windowed_data )
# Compute the magnitude spectrum,
# normalized by dividing the magnitude by N
magnitude_fft = np.abs(fft_result) / N
# Convert magnitude to decibels (dB)
magnitude_spectrum_db = 20 * np.log10(magnitude_fft + 1e-9)
# Create a frequency axis for the plot
freq_axis = np.fft.fftfreq(len(data), d=1.0/Fs)
# Plot the magnitude spectrum in dB
plt.figure(figsize=(10, 6))
plt.plot(freq_axis[:N//2], magnitude_spectrum_db[:N//2])
plt.title('Magnitude Spectrum (dB)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude (dB)')
plt.ylim(-100,100)
plt.grid(True)
plt.show()
การชักตัวอย่างจะเก็บข้อมูลตัวอย่าง (Samples) ที่มีจำนวนทั้งหมด N = 1023 ความถี่ของการชักตัวอย่างคือ Fs = 8000 Hz ดังนั้นจึงเป็นไปตามเงื่อนไขของ Nyquist กล่าวคือ อัตราการสุ่มตัวอย่าง (ในหน่วยของตัวอย่างต่อวินาที) จะต้องไม่น้อยกว่าสองเท่าของความถี่สูงสุดของสัญญาณ (กำหนดค่าความถี่ของสัญญาณไซน์คือ 500Hz)
ข้อมูลตัวอย่างจะถูกอ้างอิงโดยตัวแปรชื่อ data
และเป็นอาร์เรย์ตามรูปแบบของ
NumPy Array (numpy.array
)
ที่มีชนิดข้อมูลเป็น numpy.float32
ก่อนจะนำไปคำนวนตามรูปแบบของ
FFT จะต้องนำอาร์เรย์ของข้อมูลตัวอย่างไปคูณ (การคูณแบบทีละตัวเลข) กับอาร์เรย์ที่ได้จากฟังก์ชันหน้าต่าง
(Window Functions)
เช่น Hanning Function (numpy.hanning
)
หรือ Hamming Function (numpy.hamming
)
และผลลัพธ์ที่ได้จะถูกอ้างอิงโดยตัวแปร windowed_data
แล้วจึงนำไปใช้กับฟังก์ชัน
numpy.fft.fft()
ผลการแปลงสัญญาณข้อมูลด้วย FFT จะได้อาร์เรย์ของตัวเลขเชิงซ้อนที่มีจำนวนเท่ากับจำนวนข้อมูลตัวอย่าง ถ้าต้องการแสดงรูปกราฟของสเปกตรัมความถี่สำหรับขนาด (หรือ Magnitude) จะต้องนำไปคำนวณหาขนาดของตัวเลขเชิงซ้อนเหล่านั้น และแสดงรูปกราฟโดยมีแกนตั้งเป็นขนาด (คำนวณค่าให้มีหน่วยเป็น dB) และแกนนอนเป็นความถี่ (หน่วยเป็น Hz) เริ่มต้นที่ 0 จนถึง Fs/2
รูป: ตัวอย่างของสัญญาณไซน์ 500Hz (ไม่มีสัญญาณรบกวน) และสเปกตรัมเชิงความถี่สำหรับขนาด (Magnitude Spectrum)
รูป: ตัวอย่างของสัญญาณไซน์ 500Hz (มีสัญญาณรบกวน) และสเปกตรัมเชิงความถี่ (แมกนิจูด)
จากรูปกราฟ จะเห็นได้ว่า ขนาดของสเปกตรัมที่มีค่าสูงสุดจะตรงกับความถี่ของสัญญาณไซน์ที่เป็นสัญญาณทดสอบ
▷ PyAudio#
ถ้าต้องการอ่านค่าของสัญญาณเสียง โดยใช้ไมโครโฟนของเครื่องคอมพิวเตอร์ผู้ใช้เป็นอุปกรณ์อินพุต หรือ เซนเซอร์เสียง ก็ทำได้สะดวกเช่นกัน เนื่องจากมี PyAudio สำหรับวัตถุประสงค์ดังกล่าว
PyAudio ทำหน้าที่เป็น Python Binding สำหรับไลบรารีที่มีชื่อว่า PortAudio v19 (a cross-platform, open-source audio I/O library) ซึ่งใช้ภาษาซีในการพัฒนา ใช้ได้กับหลายแฟลตฟอร์ เช่น Windows และ Linux
ก่อนเริ่มต้นใช้งาน PyAudio จะต้องมีการติดตั้งซอฟต์แวร์ที่เกี่ยวข้องก่อน ถ้าใช้ Ubuntu (22.04 LTS) ให้ทำคำสั่งใน Bash Shell ดังนี้
## For Ubuntu 22.04 LTS.
# Show the Python 3 version
$ python3 -V
Python 3.10.12
# Install the `portaudio19-dev` library.
$ sudo apt install portaudio19-dev
# Install the PyAudio package
$ pip3 install pyaudio -U
ถัดไปเป็นตัวอย่างการเขียนโค้ด Python โดยใช้ PyAudio เพื่อแสดงข้อมูลเกี่ยวกับฮาร์ดแวร์ของระบบที่ทำหน้าที่เป็นอินพุตหรือเอาต์พุตสำหรับสัญญาณเสียง
import pyaudio
# Tested with PyAudio v0.2.14
# Create a Pyaudio class instance
p = pyaudio.PyAudio()
# Get the version of PyAudio
pyaudio_version = pyaudio.__version__
print(f"\n\nPyAudio version: {pyaudio_version}")
# Get the number of audio I/O devices
devices = p.get_device_count()
# Iterate through all audio devices
for i in range(devices):
# Get the device info
device_info = p.get_device_info_by_index(i)
# Check if this device is a microphone (an input device)
if device_info.get('maxInputChannels') > 0:
print( f"Input device: {device_info.get('name')}, " +
f"Device index: {device_info.get('index')}" )
p.terminate()
ตัวอย่างข้อความเอาต์พุต (ทดสอบกับ Ubuntu 22.04 LTS)
PyAudio version: 0.2.14
Input device: HDA Intel PCH: ALC256 Analog (hw:0,0), Device index: 0
Input device: sysdefault, Device index: 5
Input device: samplerate, Device index: 11
Input device: speexrate, Device index: 12
Input device: pulse, Device index: 13
Input device: upmix, Device index: 14
Input device: vdownmix, Device index: 15
Input device: default, Device index: 17
เมื่อได้ลองใช้ PyAudio ในเบื้องต้นแล้ว ถัดไปเป็นโค้ดตัวอย่าง Python ที่สาธิตการอ่านสัญญาณเสียง โดยใช้อุปกรณ์อินพุตเสียงของคอมพิวเตอร์ผู้ใช้ (เลือกอุปกรณ์อินพุตเป็น Default input device)
การอ่านให้ได้ข้อมูลเสียงในหนึ่งช่องสัญญาณ (Mono) จากไมโครโฟนด้วย PyAudio
จะทำให้ได้ชุดข้อมูลที่มีจำนวนข้อมูล N
เช่น เลือกจำนวนข้อมูลให้เท่ากับ 256, 512 หรือ 1024 เป็นต้น
ข้อมูลที่ได้จาก PyAudio เป็นแบบ 16-bit Signed Integer
และจะถูกแปลงให้เป็นชนิดข้อมูล numpy.float32
จำนวนของข้อมูลนั้น จะพอดีกับขนาดของ FFT
(และไม่จำเป็นต้องทำ Zero Padding)
อัตราการชักตัวอย่าง SAMPLE_RATE
มีค่าเท่ากับ 8000 (สามารถลองใช้ค่าอื่นที่สูงกว่านี้ได้
เช่น 16000 หรือ 44100 เป็นต้น)
เมื่อได้ข้อมูลครบตามจำนวน ก็จะแสดงรูปกราฟแบบ Time Series และรูปกราฟแบบสเปกตรัมความถี่ หลังจากได้คำนวณค่าด้วย FFT ในรอบถัดไป จะมีการอ่านข้อมูลชุดใหม่และอัปเดตรูปกราฟ ทุก ๆ 30 มิลลิวินาที (หรือเลือกค่าอื่นสำหรับการเว้นระยะเวลาในช่วง 20 .. 50 มิลลิวินาที)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.gridspec import GridSpec
import scipy
import signal
import pyaudio
# Print NumPy version
print(f"NumPy version: {np.__version__}")
# Tested with PyAudio v0.2.14
pyaudio_version = pyaudio.__version__
print(f"PyAudio version: {pyaudio_version}")
#-----------------------------------------------------------
# Function to handle Ctrl+C (KeyboardInterrupt)
def signal_handler(signal, frame):
print("Ctrl+C detected. Terminating the program.")
plt.close('all') # Close all matplotlib windows
if 'stream' in locals() and stream.is_active():
stream.stop_stream()
stream.close()
p.terminate()
exit(0)
def read_test_data(num_samples,fs):
f = 500
test_data = (4000*np.sin(2*np.pi*f*np.arange(num_samples)/fs)
+ 10*np.random.uniform(-1, 1, size=num_samples))
return test_data.astype(np.float32)
def read_audio_data(audio_stream, num_samples):
audio_data = audio_stream.read( num_samples )
frame_data = np.frombuffer(audio_data, dtype=np.int16)
return frame_data.astype(np.float32)
#-----------------------------------------------------------
FRAME_SIZE = 1024 # Number of samples
SAMPLE_RATE = 8000 # Set sampling rate in Hz
AUDIO_FORMAT = pyaudio.paInt16 # Audio format (16-bit PCM)
AUDIO_CHANNELS = 1 # Mono (single channel)
AUDIO_INPUT_DEVICE_ID = None # Use the default input audio device
Fs = SAMPLE_RATE # Sample rate
N = FRAME_SIZE # Number of audio samples per frame
# Create a PyAudio class instance
p = pyaudio.PyAudio()
# Create an audio stream object to get data from the microphone.
stream = p.open(
format=AUDIO_FORMAT,
channels=AUDIO_CHANNELS,
rate=SAMPLE_RATE,
input=True,
output=False,
input_device_index=AUDIO_INPUT_DEVICE_ID,
frames_per_buffer=FRAME_SIZE
)
#-----------------------------------------------------------
def initial_plot():
global fig, gs
global plot_data1, plot_data2, plot_data3
global N, Fs
# Initialize plots with subplots
fig = plt.figure(figsize=(10, 6))
fig_title = 'PyAudio Demo: Time Series Plot & FFT Spectrum'
fig.canvas.manager.set_window_title(fig_title)
gs = GridSpec(2, 1, height_ratios=[1,1])
# Adjust vertical spacing between subplots
plt.subplots_adjust(hspace=0.5)
# Create a time-domain plot for audio samples
ax1 = plt.subplot(gs[0])
ts = np.arange(0, N/Fs, 1/Fs)
plot_data1, = ax1.plot(ts, np.zeros(N) )
ax1.set_title('Samples')
ax1.set_xlabel('Time [s]')
ax1.set_ylabel('Amplitude')
ax1.set_xlim(ts[0], ts[-1]) # Set the time limits
ax1.set_ylim( -35000, 35000 ) # Set the amplitude limits
ax1.grid(False)
# Create an FFT-based frequency spectrum plot
ax2 = plt.subplot(gs[1])
freq_steps = np.fft.fftfreq(N, d=1.0/Fs)
plot_data2, = ax2.plot( freq_steps[:N//2], np.zeros(N//2) )
ax2.set_title("FFT-based Frequency Spectrum")
ax2.set_xlabel(f'Frequency (Hz): Fs={round(Fs/1e3)}kHz')
ax2.set_ylabel('Magnitude (dB)')
ax2.set_xlim(0, Fs//2) # Set frequency limits (in Hz)
ax2.set_ylim(-40, 80) # Set the magnitude limits (in dB)
ax2.grid(True)
# Function to update the plot
def update_plot(frame):
global N, Fs
global plot_data1, plot_data2, plot_data3
# Read the data
#data = read_test_data(stream,N)
audio_data = read_audio_data(stream,N)
# Apply a window function (e.g. Hanning window)
window_coeffs = np.hanning(N)
windowed_data = audio_data * window_coeffs
# Update the time-domain plot
plot_data1.set_ydata(audio_data)
# Compute FFT
spectrum = np.fft.fft(windowed_data) / N
# Update the frequency spectrum plot in logarithmic scale (dB)
spectrum_mag = 20 * np.log10( np.abs(spectrum[:N//2]) + 1e-6 )
plot_data2.set_ydata( spectrum_mag[:N//2] )
return plot_data1, plot_data2
#-----------------------------------------------------------
initial_plot()
# Register the signal handler
signal.signal(signal.SIGINT, signal_handler)
# Create animation (50 msec update interval)
ani = FuncAnimation(fig, update_plot,
blit=True, interval=30, save_count=1)
# Show the plot in non-blocking mode
plt.show()
ในการทดสอบการทำงาน ให้ลองเปิดคลิปเสียงรูปคลื่นไซน์ความถี่คงที่ เช่น ค้นหาและเลือกใช้คลิปตัวอย่างที่มีอยู่ใน Youtube เพื่อนำมาใช้เป็นสัญญาณอินพุตและทดสอบการทำงานของโค้ด Python
รูป: ตัวอย่างสเปกตรัมของเสียงที่มีความถี่คงที่ 1kHz (ไม่มีความถี่ฮาร์โมนิกแทรกเข้ามา)
รูป: ตัวอย่างสเปกตรัมของเสียงที่มีความถี่คงที่ 1kHz (มีความถี่ฮาร์โมนิกแทรกเข้ามาในอินพุตของระบบเสียง)
ถ้าลองจะลองใช้เสียงจากกีตาร์ สำหรับการตั้งค่าความถี่สำหรับเส้นสายกีตาร์ 6 เส้น ก็มีตัวอย่างคลิป เช่น Youtube: Tuning a Guitar - Standard tuning for 6 string guitar และมีการเปลี่ยนอัตราการสุ่มสัญญาณเป็น 4kHz
- E: 82 Hz (E2 Musical Note)
- A: 110 Hz (A2 Musical Note)
- D: 147 Hz (D3 Musical Note)
- G: 196 Hz (G3 Musical Note)
- B: 247 Hz (B3 Musical Note)
- E: 330 Hz (E4 Musical Note)
รูป: String 2 (B) 247 Hz
รูป: String 1 (E) 330 Hz
▷ กล่าวสรุป#
บทความนี้ได้นำเสนอตัวอย่างการเขียนโค้ด Python โดยใช้ NumPy / Matplotlib เพื่อแสดงรูปกราฟที่แสดงให้เห็นการเปลี่ยนแปลงค่าในเชิงเวลาของสัญญาณ (Time-domain Plot) และรูปกราฟซึ่งเป็นสเปกตรัมความถี่ (Magnitude Spectrum) ของสัญญาณดังกล่าวที่ได้จากการชักตัวอย่าง และการใช้ PyAudio เพื่ออ่านค่าสัญญาณเสียงจากไมโครโฟนของคอมพิวเตอร์ผู้ใช้ และใช้เป็นสัญญาณอินพุต
บทความที่เกี่ยวข้อง
- ตัวอย่างการเขียนโค้ด MATLAB และ Python สำหรับการคำนวณ FFT
- การใช้งาน ESP32 เพื่อประมวลผลข้อมูลด้วย FFT
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2023-11-16 | Last Updated: 2023-11-26