Introduction to Signal Processing: From Principles to Python Implementation

What is Signal Processing?

Signal processing is the discipline of performing various operations and analyses on signals with the aim of extracting, enhancing, or storing useful information within the signals. Signals can take various forms of data, such as sound, images, and biological electrical signals.

Classification of Signals

  1. Continuous Signals: Signals that vary continuously over time

  2. Discrete Signals: Signals sampled at specific time points

  3. Analog Signals: Signals with continuously varying amplitudes

  4. Digital Signals: Signals with quantized amplitudes

Core Concept: Fourier Transform

The Fourier Transform is one of the most important mathematical tools in signal processing, allowing us to convert signals from the time domain (temporal dimension) to the frequency domain (frequency dimension), enabling us to analyze the various frequency components contained within the signals.

Python Implementation: Signal Generation and Processing

1. Generating Composite Signals

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# Set style
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['font.sans-serif'] = ['SimHei']  
plt.rcParams['axes.unicode_minus'] = False  
# Generate signal
def generate_signal(duration=1.0, sample_rate=1000):
    """
    Generate a composite signal containing multiple frequency components
    """
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    # Generate three sine waves of different frequencies
    freq1 = 5   # 5 Hz
    freq2 = 20  # 20 Hz
    freq3 = 50  # 50 Hz
    signal1 = np.sin(2 * np.pi * freq1 * t)
    signal2 = 0.5 * np.sin(2 * np.pi * freq2 * t)
    signal3 = 0.3 * np.sin(2 * np.pi * freq3 * t)
    # Combine signals and add noise
    clean_signal = signal1 + signal2 + signal3
    noisy_signal = clean_signal + 0.2 * np.random.normal(size=clean_signal.shape)
    return t, clean_signal, noisy_signal
# Plot signals
t, clean_signal, noisy_signal = generate_signal()
plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
plt.plot(t, clean_signal)
plt.title('Clean Signal')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.subplot(2, 1, 2)
plt.plot(t, noisy_signal)
plt.title('Noisy Signal')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.tight_layout()
plt.show()

Introduction to Signal Processing: From Principles to Python Implementation

2. Fourier Analysis

def analyze_frequency(signal, sample_rate=1000):
    """
    Perform Fourier analysis on the signal
    """
    n = len(signal)
    # Calculate FFT
    fft_result = np.fft.fft(signal)
    # Calculate frequency axis
    frequencies = np.fft.fftfreq(n, 1/sample_rate)
    # Take positive frequency part
    positive_freq_idx = frequencies > 0
    frequencies = frequencies[positive_freq_idx]
    fft_result = fft_result[positive_freq_idx]
    return frequencies, np.abs(fft_result)
# Analyze clean and noisy signals in the frequency domain
freq_clean, fft_clean = analyze_frequency(clean_signal)
freq_noisy, fft_noisy = analyze_frequency(noisy_signal)
plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
plt.plot(freq_clean, fft_clean)
plt.title('Frequency Spectrum of Clean Signal')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.xlim(0, 100)
plt.subplot(2, 1, 2)
plt.plot(freq_noisy, fft_noisy)
plt.title('Frequency Spectrum of Noisy Signal')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.xlim(0, 100)
plt.tight_layout()
plt.show()

Introduction to Signal Processing: From Principles to Python Implementation

3. Filter Design and Application

def design_filter(lowcut=15, highcut=25, sample_rate=1000, order=5):
    """
    Design a bandpass filter
    """
    nyquist = sample_rate / 2
    low = lowcut / nyquist
    high = highcut / nyquist
    b, a = signal.butter(order, [low, high], btype='band')
    return b, a
def apply_filter(signal, b, a):
    """
    Apply the filter
    """
    return signal.filtfilt(b, a, signal)
# Design filter to extract signals around 20Hz
b, a = design_filter(lowcut=15, highcut=25)
# Apply filter
filtered_signal = apply_filter(noisy_signal, b, a)
plt.figure(figsize=(12, 10))
plt.subplot(3, 1, 1)
plt.plot(t, noisy_signal)
plt.title('Original Noisy Signal')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.subplot(3, 1, 2)
plt.plot(t, filtered_signal)
plt.title('Filtered Signal (15-25 Hz Bandpass)')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.subplot(3, 1, 3)
# Plot ideal 20Hz signal
ideal_20hz = 0.5 * np.sin(2 * np.pi * 20 * t)
plt.plot(t, ideal_20hz)
plt.title('Ideal 20Hz Signal (for comparison)')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.grid(True)
plt.tight_layout()
plt.show()

Introduction to Signal Processing: From Principles to Python Implementation

4. Time-Frequency Analysis: Short-Time Fourier Transform

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
import matplotlib
# Set Chinese font
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['axes.unicode_minus'] = False
def plot_spectrogram_analysis(signal_data, sample_rate=1000):
    """
    Plot the time-frequency analysis of the signal
    """
    fig, axes = plt.subplots(1, 2, figsize=(15, 6))
    # Calculate spectrogram
    f, t_spec, Sxx = signal.spectrogram(signal_data, sample_rate,                                       nperseg=256, noverlap=128)
    # Plot spectrogram
    im = axes[0].pcolormesh(t_spec, f, 10 * np.log10(Sxx + 1e-10),                           shading='gouraud', cmap='viridis')
    plt.colorbar(im, ax=axes[0], label='Power Spectral Density (dB)')
    axes[0].set_title('Spectrogram', fontsize=14, fontweight='bold')
    axes[0].set_xlabel('Time (s)')
    axes[0].set_ylabel('Frequency (Hz)')
    axes[0].set_ylim(0, 100)
    # Plot 3D time-frequency graph
    from mpl_toolkits.mplot3d import Axes3D
    # Create grid
    T, F = np.meshgrid(t_spec, f)
    ax = fig.add_subplot(122, projection='3d')
    surf = ax.plot_surface(T, F, 10 * np.log10(Sxx + 1e-10),                          cmap='viridis', alpha=0.8)
    ax.set_title('3D Time-Frequency Analysis', fontsize=14, fontweight='bold')
    ax.set_xlabel('Time (s)')
    ax.set_ylabel('Frequency (Hz)')
    ax.set_zlabel('Amplitude (dB)')
    ax.view_init(30, 45)
    plt.tight_layout()    plt.show()
# Generate test signal
def generate_test_signal():
    """Generate a test signal for time-frequency analysis"""
    duration = 2.0
    sample_rate = 1000
    t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
    # Create frequency-varying signal
    # Linear chirp signal
    chirp_signal = signal.chirp(t, f0=10, f1=80, t1=duration, method='linear')
    # Add some transient components
    transient_signal = np.zeros_like(t)
    transient_times = [0.5, 1.0, 1.5]  # seconds
    for trans_time in transient_times:
        idx = int(trans_time * sample_rate)
        transient_signal[idx:idx+100] = 1.0
    # Composite signal
    composite_signal = chirp_signal + 0.3 * transient_signal + 0.1 * np.random.normal(size=t.shape)
    return t, composite_signal
# Run time-frequency analysis
print("Performing time-frequency analysis...")
t, test_signal = generate_test_signal()
plot_spectrogram_analysis(test_signal)
print("Time-frequency analysis completed!")

Introduction to Signal Processing: From Principles to Python Implementation

5. Practical Application: Audio Signal Processing Simulation

def simulate_audio_processing():
    """
    Simulate audio signal processing
    """
    # Create a longer signal to simulate audio
    duration = 5.0
    sample_rate = 44100
    t_audio = np.linspace(0, duration, int(sample_rate * duration))
    # Create chord signal (C major chord: C4, E4, G4)
    frequencies = [261.63, 329.63, 392.00]  # C4, E4, G4
    amplitudes = [1.0, 0.7, 0.5]
    chord_signal = np.zeros_like(t_audio)
    for freq, amp in zip(frequencies, amplitudes):
        chord_signal += amp * np.sin(2 * np.pi * freq * t_audio)
    # Add envelope
    envelope = np.ones_like(t_audio)
    attack = int(0.5 * sample_rate)
    release = int(0.5 * sample_rate)
    sustain = len(t_audio) - attack - release
    envelope[:attack] = np.linspace(0, 1, attack)
    envelope[attack:attack+sustain] = 1
    envelope[attack+sustain:] = np.linspace(1, 0, release)
    chord_signal *= envelope
    noisy_chord = chord_signal + 0.2 * np.random.normal(size=chord_signal.shape)
    return t_audio, chord_signal, noisy_chord, frequencies
# Generate simulated audio signal
t_audio, chord_clean, chord_noisy, note_freqs = simulate_audio_processing()
# Analyze audio signal
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Time domain signal
axes[0, 0].plot(t_audio, chord_clean, 'b-', linewidth=1, alpha=0.8)
axes[0, 0].set_title('Clean Chord Signal - Time Domain', fontsize=12, fontweight='bold')
axes[0, 0].set_xlabel('Time (s)')
axes[0, 0].set_ylabel('Amplitude')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].set_xlim(0, 1)  # Show only the first second
axes[0, 1].plot(t_audio, chord_noisy, 'r-', linewidth=1, alpha=0.8)
axes[0, 1].set_title('Noisy Chord Signal - Time Domain', fontsize=12, fontweight='bold')
axes[0, 1].set_xlabel('Time (s)')
axes[0, 1].set_ylabel('Amplitude')
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].set_xlim(0, 1)
# Frequency domain analysis
freq_audio, mag_audio, _ = frequency_analysis(chord_clean, 44100)
freq_noisy_audio, mag_noisy_audio, _ = frequency_analysis(chord_noisy, 44100)
axes[1, 0].plot(freq_audio, mag_audio, 'b-', linewidth=2)
# Mark note frequencies
for freq in note_freqs:
    axes[1, 0].axvline(x=freq, color='red', linestyle='--', alpha=0.7,                       label=f'{freq:.1f}Hz')
axes[1, 0].set_title('Frequency Spectrum of Clean Chord', fontsize=12, fontweight='bold')
axes[1, 0].set_xlabel('Frequency (Hz)')
axes[1, 0].set_ylabel('Amplitude')
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].set_xlim(200, 500)
axes[1, 1].plot(freq_noisy_audio, mag_noisy_audio, 'r-', linewidth=2)
for freq in note_freqs:
    axes[1, 1].axvline(x=freq, color='blue', linestyle='--', alpha=0.7)
axes[1, 1].set_title('Frequency Spectrum of Noisy Chord', fontsize=12, fontweight='bold')
axes[1, 1].set_xlabel('Frequency (Hz)')
axes[1, 1].set_ylabel('Amplitude')
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].set_xlim(200, 500)
plt.tight_layout()
plt.show()

Introduction to Signal Processing: From Principles to Python Implementation

Leave a Comment