ตัวอย่างการเขียนโค้ด Python เพื่ออ่านข้อมูลเสียงจากไมโครโฟน#


การทดลองใช้ไลบรารี PyAudio#

โค้ด Python ต่อไปนี้สาธิตการใช้คำสั่งหรือฟังก์ชันของ PyAudio โดยให้แสดงรายการของอุปกรณ์อินพุตในระบบ เช่น อินพุตจากไมโครโฟน (Microphone) ซึ่งอาจจะเป็น Built-in Microphone หรือ USB Microphone หรือ Bluetooth Microphone ก็ได้

ก่อนทดลองใช้โค้ดตัวอย่าง แนะนำใช้ VS Code IDE และติดตั้ง Python Extension for Visual Studio Code (by Microsoft) และให้สร้างไดเรกทอรีใหม่ และสร้าง Python Virtual Environment เพื่อติดตั้งแพ็คเกจต่าง ๆ ที่จะต้องใช้ต่อไปนี้

$ pip3 install pyaudio matplotlib numpy

ในกรณีที่ใช้ระบบปฏิบัติการ Ubuntu (เช่น 22.04) จะต้องมีการติดตั้งไลบรารีสำหรับ Linux ดังนี้ ก่อนติดตั้ง PyAudio

$ sudo apt install portaudio19-dev

ในบทความนี้ ได้ลองใช้ PyAudio เวอร์ชัน v0.2.14 ร่วมกับ Python 3.10.10 และทดลองเปิดใช้งานไมโครโฟนของระบบที่มีการตั้งค่าตัวเลือกไว้เป็น default

  • มีการตั้งค่าอัตราในการชักตัวอย่าง (Sample Rate) ให้เท่ากับ 16kHz
  • เลือกช่องสัญญาณเสียงแบบ Mono ไม่ใช่ Stereo ข้อมูลที่ได้จะเป็นเลขจำนวนเต็มขนาด 16 บิต (int16)
  • อ่านข้อมูลมาเก็บไว้ในบัฟเฟอร์ที่มีความจุ (Buffer Length) เท่ากับ 1024 ข้อมูลเหล่านี้จะถูกแปลงให้เป็นอาร์เรย์ของข้อมูลแบบ np.int16 ด้วย NumPy Array
import pyaudio
import numpy as np

# Create PyAudio instance
p = pyaudio.PyAudio()

# List all available devices and print them
default_device_index = None
device_count = p.get_device_count()
print("\n\nInput device list: ")
for i in range(device_count):
    device_info = p.get_device_info_by_index(i)
    print( f"- Device {i}: {device_info['name']}", 
           f"Input Channels: {device_info['maxInputChannels']}" )
    if device_info['name'] == 'default':
        default_device_index = i

# Audio stream parameters
FORMAT      = pyaudio.paInt16  # 16-bit audio format
CHANNELS    = 1                # Mono audio
SAMPLE_RATE = 16000            # Sample rate (16kHz)
BUF_LEN     = 1024             # Number of frames per buffer
GAIN        = 2.0              # Amplification gain factor

# Try to open the selected input device
# Open an audio stream for reading (microphone input)
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                input_device_index=default_device_index,                
                frames_per_buffer=BUF_LEN)

if stream:
    print("Try to read audo samples...")
    audio_data = np.frombuffer(stream.read(BUF_LEN), dtype=np.int16)
    print( audio_data )
    stream.close()

print( 'Done' )

รูป: ตัวอย่างการใช้ VS Code IDE และรันโค้ดตัวอย่าง


การแสดงรูปคลื่นสัญญาณเสียงในโดเมนเวลา#

ตัวอย่างโค้ดถัดไป สาธิตการอ่านข้อมูลเสียง ชุดละ 1024 ตัวเลขมีขนาด 16 บิต แล้วนำมาแสดงผลเป็นรูปกราฟสัญญาณเสียงตามลำดับ โดยใช้คำสั่งของ matplotlib และใช้ TkInter สำหรับส่วนแสดงผลเชิงกราฟสำหรับการทำงานของ Python ข้อมูลเสียงและกราฟ จะถูกอัปเดทและแสดงผลทุก ๆ 20 มิลลิวินาที

import warnings
warnings.filterwarnings("ignore") # Suppress all warnings

import sys
import pyaudio
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk

# Audio stream parameters
FORMAT      = pyaudio.paInt16  # 16-bit audio format
CHANNELS    = 1                # Mono audio
SAMPLE_RATE = 16000            # Sample rate (16kHz)
BUF_LEN     = 1024             # Number of frames per buffer
GAIN        = 4.0              # Amplification gain factor

# Create PyAudio instance
p = pyaudio.PyAudio()

# Open an audio stream for reading (microphone input)
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                input_device_index=None,
                frames_per_buffer=BUF_LEN)

# Set the value range (16-bit audio samples): y_min and y_max
value_range = [-2**15, 2**15]  

# Create a subplot
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(0, BUF_LEN)
line, = ax.plot(x, np.random.rand(BUF_LEN))
ax.set_ylim(value_range[0], value_range[1])
ax.set_xlim(0, BUF_LEN)
ax.grid(True, linestyle='--', color='gray', linewidth=0.5)
ax.set_xlabel("Sample Index")  # X-axis label
ax.set_ylabel("Amplitude")  # Y-axis label

# Set up the Tkinter window
root = tk.Tk()
root.title("Real-Time Audio Waveform")
#root.geometry("1024x576")

# Embed Matplotlib figure into Tkinter window using FigureCanvasTkAgg
canvas = FigureCanvasTkAgg(fig, master=root)  
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

def on_close():
    sys.exit()

# Update function for real-time visualization
def update_plot():
    try:
        samples = stream.read(BUF_LEN, exception_on_overflow=False)
        audio_data =  GAIN * np.frombuffer(samples, dtype=np.int16)
        audio_data = np.clip(audio_data, value_range[0], value_range[1])
        # Update time-domain plot
        line.set_ydata(audio_data)

        # Update draw canvas
        canvas.draw()
        root.after(20, update_plot)  # Schedule next update
    except KeyboardInterrupt as e:
        on_close()

root.protocol("WM_DELETE_WINDOW", on_close)

try:
    update_plot()  # Start the real-time plotting
    root.mainloop()  # Start the Tkinter main loop
except KeyboardInterrupt:
    root.quit()
    root.destroy()

รูป: การแสดงรูปคลื่นสัญญาณเสียงโดยใช้โค้ดตัวอย่าง


การแสดงรูปคลื่นสัญญาณเสียงในโดเมนเวลาและความถี่#

โค้ดตัวอย่างถัดไปสาธิตการแสดงรูปคลื่นสัญญาณที่มีการเปลี่ยนแปลงในเชิงเวลา และแสดงสเปกตรัมเชิงความถี่ของสัญญาณ โดยใช้การแปลงแบบ FFT (Fast-Fourier Transform)

import warnings
warnings.filterwarnings("ignore")  # Suppress all warnings

import sys
import pyaudio
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk

# Audio stream parameters
FORMAT = pyaudio.paInt16  # 16-bit audio format
CHANNELS = 1  # Mono audio
SAMPLE_RATE = 16000  # Sample rate (16kHz)
BUF_LEN = 1024  # Number of frames per buffer
GAIN = 4.0  # Amplification gain factor

# Create PyAudio instance
p = pyaudio.PyAudio()

# Open an audio stream for reading (microphone input)
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                input_device_index=None,
                frames_per_buffer=BUF_LEN)

# Set the value range (16-bit audio samples): y_min and y_max
value_range = [-2**15, 2**15]  

# Create figure and subplots
fig, (ax_time, ax_freq) = plt.subplots(2, 1, figsize=(10, 8))
x_time = np.arange(0, BUF_LEN)
line_time, = ax_time.plot(x_time, np.random.rand(BUF_LEN))
ax_time.set_ylim(value_range[0], value_range[1])
ax_time.set_xlim(0, BUF_LEN)
ax_time.grid(True, linestyle='--', color='gray', linewidth=0.5)
ax_time.set_xlabel("Sample Index")
ax_time.set_ylabel("Amplitude")
ax_time.set_title("Time-Domain Waveform")

# FFT-based spectrum
x_freq = np.fft.rfftfreq(BUF_LEN, d=1.0/SAMPLE_RATE)
line_freq, = ax_freq.plot(x_freq, np.zeros_like(x_freq))
ax_freq.set_xlim(0, SAMPLE_RATE / 2)
ax_freq.set_ylim(0, 1)
ax_freq.grid(True, linestyle='--', color='gray', linewidth=0.5)
ax_freq.set_xlabel("Frequency (Hz)")
ax_freq.set_ylabel("Magnitude")
ax_freq.set_title("Frequency-Domain Spectrum")

# Set up the Tkinter window
root = tk.Tk()
root.title("Real-Time Audio Visualization")

# Embed Matplotlib figure into Tkinter window
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

def on_close():
    sys.exit()

def update_plot():
    try:
        samples = stream.read(BUF_LEN, exception_on_overflow=False)
        audio_data = GAIN * np.frombuffer(samples, dtype=np.int16)
        audio_data = np.clip(audio_data, value_range[0], value_range[1])        
        # Update time-domain plot
        line_time.set_ydata(audio_data)

        # Compute and update frequency-domain plot (FFT)
        fft_data = np.abs(np.fft.rfft(audio_data)) / BUF_LEN
        line_freq.set_ydata(fft_data)
        ax_freq.set_ylim(0, np.max(fft_data) * 1.1)

        # Update draw canvas
        canvas.draw()
        root.after(20, update_plot)  # Schedule next update
    except KeyboardInterrupt as e:
        on_close()

root.protocol("WM_DELETE_WINDOW", on_close)

try:
    update_plot()
    root.mainloop()
except KeyboardInterrupt:
    root.quit()
    root.destroy()
finally:
    on_close()

รูป: การแสดงรูปคลื่นสัญญาณเสียงในโดเมนเวลาและความถี่ (ทดลองใช้สัญญาณเสียงทดสอบ ความถี่ 1kHz ซึ่งจะเห็นได้ว่า สเปกตรัมจะมีขนาดสูงสุดตรงความถี่ดังกล่าว)


การแสดงรูปคลื่นสัญญาณเสียงและบันทึกข้อมูลลงไฟล์ .wav#

ถัดไปเป็นตัวอย่างโค้ดสำหรับการอ่านข้อมูลเสียงและแสดงรูปคลื่นสัญญาณ ผู้ใช้สามารถกดปุ่มเริ่มบันทึกข้อมูลเสียง (Start) และหยุดการบันทึกเสียง (Stop) แต่จะบันทึกเสียงไม่เกิน 5 วินาที หรือ จะกดปุ่มรีเซต (Reset) เพื่อเคลียร์ข้อมูลในบัฟเฟอร์ หากกดปุ่ม Save ก็จะได้ไฟล์เอาต์พุต เป็นไฟล์ .wav (ชื่อ sound.wav)

import sys
import time
import pyaudio
import wave
import numpy as np
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import threading

#####################################################################
# Constants
FORMAT = pyaudio.paInt16  # 16-bit audio format
CHANNELS = 1              # Mono audio
SAMPLE_RATE = 16000       # Sample rate (16kHz)
BUF_LEN = 1024            # Number of frames per buffer
GAIN = 5.0
AMPLITUDE_RANGE = [-2**15, 2**15]  # For 16-bit audio samples

#####################################################################

# Matplotlib figure and axis setup for waveform visualization
fig, ax = plt.subplots(figsize=(9,3))
x = np.arange(0, BUF_LEN)
line, = ax.plot(x, np.random.rand(BUF_LEN))
ax.set_ylim(AMPLITUDE_RANGE[0], AMPLITUDE_RANGE[1])
ax.set_xlim(0, BUF_LEN)
ax.grid(True, linestyle='--', color='gray', linewidth=0.5)

#####################################################################
# PyAudio settings

p = pyaudio.PyAudio()
audio_data = []
duration = 5  # 5 seconds
stop_time = time.time() + 1e6
stop_recording = False

# Open the audio stream
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                frames_per_buffer=BUF_LEN)

#####################################################################
# Tk callback functions

# Audio start/stop/reset/save functions
def start_cb():
    global audio_data, stop_recording, stop_time, duration
    stop_recording = False
    stop_time = time.time() + duration
    audio_data = [] # Clear saved audio data
    print("Start")

def stop_cb():
    global stop_recording
    stop_recording = True
    print("Stop")

def reset_cb():
    global audio_data, stop_recording
    stop_recording = False
    audio_data = []  # Clear audio data
    line.set_ydata(np.random.rand(BUF_LEN))
    canvas.draw()
    print("Reset")

def save_cb():
    global audio_data
    print("Save")
    if not audio_data:
        print("No audio data to save.")
        return
    # Flatten audio data
    audio_data_flat = np.concatenate(audio_data)
    # Save audio data as a .wav file
    filename = "sound.wav"
    with wave.open(filename, 'wb') as wf:
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(p.get_sample_size(FORMAT))
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(audio_data_flat.tobytes())    
    print(f"Saved audio data to: {filename}")

#####################################################################
# Tkinter setup
root = tk.Tk()
root.title("Audio Recorder")

# Embed Matplotlib figure into Tkinter window using FigureCanvasTkAgg
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(padx=10, pady=10, fill="both", expand=True)

# Frame for the controls (buttons and duration input)
control_frame = ttk.Frame(root)
control_frame.pack(fill="y", padx=20, pady=20) 

# Start, Stop, Reset, Save Buttons
start_button = ttk.Button(control_frame, text="Start", command=start_cb)
start_button.pack(side="left", padx=10, pady=5)

stop_button = ttk.Button(control_frame, text="Stop", command=stop_recording)
stop_button.pack(side="left", padx=10, pady=5)

reset_button = ttk.Button(control_frame, text="Reset", command=reset_cb)
reset_button.pack(side="left", padx=10, pady=5)

save_button = ttk.Button(control_frame, text="Save", command=save_cb)
save_button.pack(side="left", padx=10, pady=5)

# Update the waveform plot
def update_waveform(audio_chunk):
    line.set_ydata(audio_chunk)
    canvas.draw()

# Continuous audio visualization
def visualize_audio():
    global stream, audio_data, duration, stop_recording
    max_len = SAMPLE_RATE*duration
    while True:
        audio_chunk = np.frombuffer(stream.read(BUF_LEN), dtype=np.int16)
        if time.time() >= stop_time:
            stop_recording = True
        if not stop_recording:
            audio_data.append(audio_chunk)
            while len(audio_data) > max_len:
                audio_data.pop(0)
        # Update the waveform
        update_waveform(audio_chunk)
        time.sleep(0.01)

# Start the visualization thread (before the GUI loop)
threading.Thread(target=visualize_audio, daemon=True).start()

def on_close():
    sys.exit()

root.protocol("WM_DELETE_WINDOW", on_close)

try:
    # Start Tkinter main loop
    root.mainloop()
except KeyboardInterrupt:
    root.quit()
    root.destroy()
finally:
    on_close()

รูป: ตัวอย่างการทำงานของโค้ด


การสื่อสารข้อมูลเสียงระหว่างคอมพิวเตอร์ด้วยโพรโตคอล UDP#

ตัวอย่างถัดไปสาธิตการเขียนโค้ด โดยแบ่งการทำงานออกเป็น 2 ส่วน และมีสถาปัตยกรรมการทำงานแบบ Server-Client Model และสื่อสารข้อมูลกันผ่านระบบเครือข่ายได้ ด้วยโพรโตคอลที่เรียกว่า UDP (User Datagram Protocol)

  • ส่วนแรกทำหน้าที่เป็น UDP Server อ่านข้อมูลเสียงจากไมโครโฟน โดยใช้ PyAudio และรอให้มีการเชื่อมต่อเข้ามาโดย UDP Receiver เพื่อส่งข้อมูลเสียงกลับไป
  • ส่วนที่สองทำหน้าที่เป็น UDP Client คอยรับข้อมูลจาก UDP Server เพื่อนำข้อมูลดังกล่าวมาแสดงรูปคลื่นสัญญาณเสียง

ในโค้ดตัวอย่างได้เลือกใช้พอร์ตหมายเลข 5005 สำหรับการสื่อสารข้อมูลด้วย UDP จำนวนข้อมูลในแต่ละครั้งของการอ่านและส่งข้อมูลคือ 1024 และใช้อัตราการชักตัวอย่าง Sample Rate เท่ากับ 16kHz เป็นตัวอย่าง

โค้ด: audio_udp_server.py

import warnings
import time
import socket
import pyaudio
import numpy as np

warnings.filterwarnings("ignore")  # Suppress all warnings

# Audio stream parameters
FORMAT = pyaudio.paInt16  # 16-bit audio format
CHANNELS = 1  # Mono audio
SAMPLE_RATE = 16000  # Sample rate (16kHz)
BUF_LEN = 1024  # Number of frames per buffer
GAIN = 4.0  # Amplification gain factor

# UDP server parameters
UDP_IP = "0.0.0.0"  # Listen on all available interfaces
UDP_PORT = 5005  # Port number

# Create PyAudio instance
p = pyaudio.PyAudio()

# Open an audio stream for reading (microphone input)
stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=SAMPLE_RATE,
                input=True,
                frames_per_buffer=BUF_LEN)

# Create UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((UDP_IP, UDP_PORT))

print(f"UDP server listening on {UDP_IP}:{UDP_PORT}")

try:
    while True:
        data = None
        try:
            # Wait for a client request
            data, addr = sock.recvfrom(1024)  # Buffer size is 1024 bytes
        except ConnectionResetError:
            pass
        if data:
            print(f"Received request from {addr}")
            samples = stream.read(BUF_LEN, exception_on_overflow=False)
            sock.sendto(samples, addr)  # Send audio data to client
except KeyboardInterrupt:
    print("Server shutting down...")
finally:
    stream.stop_stream()
    stream.close()
    p.terminate()
    sock.close()

โค้ด: audio_udp_client.py

import sys
import socket
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Audio stream parameters
FORMAT      = pyaudio.paInt16  # 16-bit audio format
CHANNELS    = 1  # Mono audio
SAMPLE_RATE = 16000  # Sample rate (16kHz)
BUF_LEN     = 1024  # Number of frames per buffer
GAIN        = 4.0  # Amplification gain factor

# Create UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(1.0)

# Send initial request to server
sock.sendto(b"START", (SERVER_IP, SERVER_PORT))

# Initialize Tkinter window
root = tk.Tk()
root.title("Real-time Audio Waveform")

# Create Matplotlib figure
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(BUF_LEN)
y = np.zeros(BUF_LEN)
line, = ax.plot(x, y, lw=2)
ax.set_ylim(-32768, 32767)
ax.set_xlim(0, BUF_LEN)
ax.set_xlabel("Sample Index")
ax.set_ylabel("Amplitude")
ax.set_title("Live Audio Waveform")

# Embed Matplotlib in Tkinter
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

def on_close():
    sys.exit()

def update_plot(frame):
    try:
        sock.sendto(b"REQ", (SERVER_IP, SERVER_PORT))  # Request new data
        data, _ = sock.recvfrom(BUF_LEN * SAMPLE_WIDTH)  # Receive data
        samples = np.array(struct.unpack(f"{BUF_LEN}{FORMAT}", data))
        line.set_ydata(samples)
    except socket.timeout:
        pass
    except ConnectionResetError:
        print("UDP server connection failed!")
        on_close()
    except KeyboardInterrupt:
        on_close()
    return line,

# Animate the plot
ani = animation.FuncAnimation(fig, update_plot, 
                              interval=20, blit=True, save_count=1)

root.protocol("WM_DELETE_WINDOW", on_close)

try:
    # Start Tkinter main loop
    root.mainloop()
except KeyboardInterrupt:
    root.quit()
    root.destroy()
    on_close()

ในการทดลอง ให้รันโค้ด audio_udp_server.py และ audio_udp_client.py เพื่อให้ทั้งสองโปรแกรมทำงานพร้อม ๆ กัน ในเครื่องคอมพิวเตอร์เดียวกัน แต่ถ้ารันโปรแกรมต่างเครื่องคอมพิวเตอร์กัน จะต้องมีการระบุหมายเลข IP ให้ถูกต้องด้วย

 


กล่าวสรุป#

บทความนี้ได้นำเสนอโค้ดตัวอย่างในภาษา Python เพื่อสาธิตการใช้งาน PyAudio ในเบื้องต้น เพื่ออ่านข้อมูลเสียงจากไมโครโฟน มีการนำข้อมูลที่อ่านได้มาแสดงรูปคลื่นสัญญาณในเชิงเวลา และสเปกตรัมเชิงความถี่ด้วยการคำนวณตามวิธีการที่เรียกว่า FFT (Fast Fourier Transform) มีตัวอย่างการเขียนโค้ดเพื่อบันทึกข้อมูลเสียงให้เป็นไฟล์ .wav และยังมีตัวอย่างการรับส่งข้อมูลเสียงผ่านเครือข่ายด้วย UDP (User Datagram Protocol)

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2025-03-26 | Last Updated: 2025-03-27