পাঠ ২৯ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Digital Signal Processing / FFT-এর প্রয়োগ

FFT-এর প্রয়োগ — বাস্তবে স্পেকট্রাল অ্যানালাইসিস

FFT applications — spectral analysis in practice
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কীভাবে একটি বাস্তবসম্মত (নয়েজযুক্ত, একাধিক ফ্রিকোয়েন্সির) সিন্থেটিক টেস্ট সিগন্যাল বানাতে হয়
  • FFT বিন-ইনডেক্স থেকে প্রকৃত ফ্রিকোয়েন্সি (Hz)-এ রূপান্তরের সূত্র
  • একটি সত্যিকারের, কোডে-লেখা পিক-ফাইন্ডিং পদ্ধতি — শুধু চোখে দেখে অনুমান নয়
  • FFT-ভিত্তিক স্পেকট্রাল অ্যানালাইসিসের বাস্তব প্রয়োগ (স্পেকট্রাম অ্যানালাইজার, টিউনার, EQ ইত্যাদির পেছনের ধারণা)

১ · একটি বাস্তবসম্মত সিন্থেটিক টেস্ট সিগন্যাল

এখন পর্যন্ত (L27-L28) আমরা পরিষ্কার, নয়েজবিহীন সিগন্যালে FFT পরীক্ষা করেছি। বাস্তবে সিগন্যাল প্রায় কখনোই এত পরিষ্কার হয় না — সেন্সর নয়েজ, ইলেকট্রনিক নয়েজ ইত্যাদি সবসময় কিছুটা এলোমেলো উপাদান যোগ করে। নিচে আমরা N = 128 স্যাম্পল, fs = 128 Hz স্যাম্পল রেটে একটি সিগন্যাল বানাচ্ছি যেখানে দুটি সত্যিকারের ফ্রিকোয়েন্সি উপাদান আছে (f₁ = 10 Hz, অ্যামপ্লিটিউড ১.০; আর f₂ = 30 Hz, অ্যামপ্লিটিউড ০.৬) — প্লাস random.gauss(0, 0.3) দিয়ে যোগ করা নয়েজ (একটি fixed seed = 42 দিয়ে, যাতে ফলাফল প্রতিবার একই হয়, M11-এ বিস্তারিত আলোচিত র‍্যান্ডম-সিগন্যাল কনভেনশন অনুসরণ করে)।

দুটি লুকানো টোন
১০ Hz (শক্তিশালী) ও ৩০ Hz (দুর্বল) — বাস্তবে এমন হতে পারে দুটো যন্ত্রের একসাথে বাজানো সুর, বা দুটো সেন্সরের কম্পন।
fixed-seed নয়েজ
random.seed(42) ব্যবহার করায় প্রতিবার একই "এলোমেলো" নয়েজ তৈরি হয় — ফলাফল রিপ্রোডিউসিবল, ডিবাগযোগ্য।
fs/N = ১ Hz রেজোলিউশন
N=128, fs=128 Hz বেছে নেওয়ায় প্রতিটি FFT বিন ঠিক ১ Hz প্রতিনিধিত্ব করে (L25-এর ফ্রিকোয়েন্সি-রেজোলিউশন ধারণা) — তাই বিন ১০ = ঠিক ১০ Hz।

২ · FFT দিয়ে স্পেকট্রাম বের করা

নিচের কোডে L28-এর iterative_fft ফাংশনটি পুনরায় লেখা হয়েছে (এই পাঠকে স্বয়ংসম্পূর্ণ রাখতে), তারপর সিগন্যাল বানিয়ে FFT চালিয়ে সিঙ্গল-সাইডেড ম্যাগনিটিউড স্পেকট্রাম (|X[k]|/N × 2, যাতে ম্যাগনিটিউড সরাসরি প্রকৃত অ্যামপ্লিটিউডের সাথে তুলনীয় হয়) বের করা হয়েছে:

Python
import cmath, math, random

def bit_reverse_indices(N):
    bits = N.bit_length() - 1
    order = []
    for i in range(N):
        rev = 0
        v = i
        for _ in range(bits):
            rev = (rev << 1) | (v & 1)
            v >>= 1
        order.append(rev)
    return order

def iterative_fft(x):
    N = len(x)
    order = bit_reverse_indices(N)
    X = [x[order[i]] for i in range(N)]
    size = 2
    while size <= N:
        half = size // 2
        twiddles = [cmath.exp(-2j * math.pi * j / size) for j in range(half)]
        for start in range(0, N, size):
            for j in range(half):
                w = twiddles[j]
                even_part = X[start + j]
                odd_part = X[start + j + half] * w
                X[start + j] = even_part + odd_part
                X[start + j + half] = even_part - odd_part
        size *= 2
    return X

random.seed(42)
N = 128
fs = 128.0
f1, a1 = 10.0, 1.0
f2, a2 = 30.0, 0.6
noise_sigma = 0.3

x = [a1 * math.sin(2 * math.pi * f1 * n / fs) + a2 * math.sin(2 * math.pi * f2 * n / fs)
     + random.gauss(0, noise_sigma) for n in range(N)]

X = iterative_fft(x)
mag = [abs(X[k]) / N * 2 for k in range(N // 2)]

print(f"{'বিন k':>6} | {'ফ্রিকোয়েন্সি (Hz)':>16} | {'ম্যাগনিটিউড':>10}")
for k in range(0, N // 2):
    freq = k * fs / N
    if k < 3 or (7 <= k <= 13) or (27 <= k <= 33) or k in (40, 50, 60):
        print(f"{k:>6} | {freq:>16.2f} | {mag[k]:>10.4f}")

    
টেবিলের একটি অংশ রান করলে দেখা যায় — বিন ০-২ (DC-এর কাছাকাছি) আর বিন ৪০, ৫০, ৬০-এ ম্যাগনিটিউড ছোট (০.০১ থেকে ০.০৮-এর মধ্যে, শুধু নয়েজের অবদান), কিন্তু বিন ১০ (১০.০০ Hz)-এ ম্যাগনিটিউড ০.৯৭২৪ — সত্যিকারের অ্যামপ্লিটিউড ১.০-এর কাছাকাছি — আর বিন ৩০ (৩০.০০ Hz)-এ ০.৫৬৮৭ — সত্যিকারের অ্যামপ্লিটিউড ০.৬-এর কাছাকাছি। দুটো পিকই বাকি নয়েজ-বিনগুলোর তুলনায় অনেক বড় ও স্পষ্টভাবে আলাদা।

৩ · জেনুইন পিক-ফাইন্ডিং

চোখে দেখে "ওই দুটো বিন বড়" বলাটা মানুষের জন্য সহজ, কিন্তু একটি বাস্তব সিস্টেমকে এটি স্বয়ংক্রিয়ভাবে করতে হয়। নিচের কোড একটি সহজ কিন্তু বৈধ পিক-ফাইন্ডিং নিয়ম প্রয়োগ করে: একটি বিন তখনই "পিক" যদি তার ম্যাগনিটিউড তার দুই পাশের প্রতিবেশীর চেয়ে বেশি হয় (লোকাল ম্যাক্সিমা) এবং একটি থ্রেশহোল্ড (0.15, নয়েজ-বিনগুলোর সর্বোচ্চ মান ~০.০৮-এর প্রায় দ্বিগুণ, যাতে নয়েজকে ভুলবশত পিক না ধরা হয়) ছাড়িয়ে যায়:

Python
threshold = 0.15
peaks = []
for k in range(1, N // 2 - 1):
    if mag[k] > mag[k - 1] and mag[k] > mag[k + 1] and mag[k] > threshold:
        peaks.append((k, k * fs / N, mag[k]))

print("শনাক্ত হওয়া পিক (bin, freq Hz, magnitude):")
for p in peaks:
    print(p)

top2 = sorted(peaks, key=lambda p: -p[2])[:2]
top2_by_freq = sorted(top2, key=lambda p: p[1])
recovered = [f for _, f, _ in top2_by_freq]

print()
print(f"প্রকৃত ফ্রিকোয়েন্সি: {f1} Hz, {f2} Hz")
print(f"ফ্রিকোয়েন্সি রেজোলিউশন fs/N = {fs/N} Hz")
print(f"পুনরুদ্ধার করা ফ্রিকোয়েন্সি: {recovered}")
print(f"ত্রুটি: {[abs(recovered[i] - [f1, f2][i]) for i in range(len(recovered))]}")

    
এই কোডটি আগের কোড সেলের mag, N, fs, f1, f2 চলকের উপর নির্ভরশীল (আগের সেল আগে রান করা দরকার)। রান করলে অ্যালগরিদম ঠিক দুটো পিক খুঁজে পায় — (10, 10.0, 0.9724) আর (30, 30.0, 0.5687) — অন্য কোনো নয়েজ-বিন থ্রেশহোল্ড অতিক্রম করেনি। পুনরুদ্ধার করা ফ্রিকোয়েন্সি তালিকা [10.0, 30.0], আর প্রতিটির ত্রুটি একদম ০.০ — কারণ এই নির্দিষ্ট উদাহরণে fs/N = 1.0 Hz রেজোলিউশন হওয়ায় ১০ Hz আর ৩০ Hz উভয়েই একটি বিন-কেন্দ্রের ওপর পড়ে যায় বলে বিন থেকে ফ্রিকোয়েন্সিতে রূপান্তর (freq = k × fs/N) কোনো রাউন্ডিং ত্রুটি ছাড়াই সঠিক মান দেয়। যদি প্রকৃত ফ্রিকোয়েন্সি কোনো বিন-কেন্দ্রের ঠিক মাঝে পড়ত (যেমন ১০.৫ Hz), তখন স্পেকট্রাল লিকেজ-এর কারণে (L24-এ বিস্তারিত) নিকটতম বিনই সবচেয়ে বড় ম্যাগনিটিউড দেখাত, ফলে সামান্য ত্রুটি (সর্বোচ্চ অর্ধেক-বিন, এখানে ০.৫ Hz) থাকত।

৪ · সীমাবদ্ধতা ও বাস্তব প্রয়োগ

এই ডেমোর নির্ভুলতা (শূন্য ত্রুটি) আংশিকভাবে সৌভাগ্যবশত — কারণ ফ্রিকোয়েন্সি রেজোলিউশন যথাযথভাবে বেছে নেওয়া হয়েছিল। বাস্তব প্রয়োগে ফ্রিকোয়েন্সি সবসময় বিন-কেন্দ্রের ওপর পড়বে না, তাই M5/L25-এর zero-padding (বিন ঘনত্ব বাড়ায়, কিন্তু সত্যিকারের রেজোলিউশন বাড়ায় না) আর L24-এর windowing (স্পেকট্রাল লিকেজ কমায়) একসাথে ব্যবহার করা হয় নির্ভুলতা বাড়াতে। এই ধরনের FFT-ভিত্তিক পিক-ফাইন্ডিং বাস্তবে অনেক জায়গায় ব্যবহৃত হয় — মিউজিক টিউনার (একটি বাজানো সুরের পিচ শনাক্ত করা), স্পেকট্রাম অ্যানালাইজার, অডিও ইকুয়ালাইজার (কোন ফ্রিকোয়েন্সি বেশি/কম আছে তা দেখে সমন্বয় করা), এমনকি কম্পন-বিশ্লেষণ (যন্ত্রের কোনো অংশ ভুল ফ্রিকোয়েন্সিতে কাঁপছে কিনা তা শনাক্ত করা)। M10-এ আমরা এই একই কৌশল আরও এগিয়ে নিয়ে যাব — STFT (L45) দিয়ে সময়ের সাথে সাথে পরিবর্তিত স্পেকট্রাম দেখা, আর পাওয়ার স্পেকট্রাল ডেনসিটি (L44) দিয়ে নয়েজ ফ্লোরের উপরে সিগন্যাল কতটা স্পষ্ট তা পরিমাপ করা।

মূল কথা · Key takeaway

M6 জুড়ে (L26-L29) আমরা দেখলাম — কেন ডিরেক্ট DFT-এর O(N²) খরচ একটি সমস্যা (L26), কীভাবে বাটারফ্লাই সূত্র দিয়ে এটি রিকার্সিভভাবে O(N log N)-এ কমানো যায় ও ডিরেক্ট DFT-এর সাথে যাচাই করা যায় (L27), কীভাবে একই অ্যালগরিদম ইটারেটিভভাবে বিট-রিভার্সাল + বাটারফ্লাই দিয়ে বানিয়ে তিন-মুখী ক্রস-চেক ও সত্যিকারের অপারেশন-কাউন্ট তুলনা করা যায় (L28), আর এই পাঠে দেখলাম কীভাবে সেই FFT একটি নয়েজযুক্ত বাস্তবসম্মত সিগন্যাল থেকে লুকানো ফ্রিকোয়েন্সি নির্ভুলভাবে খুঁজে বের করতে পারে। পরবর্তী মডিউলে (M7) আমরা ফ্রিকোয়েন্সি-ডোমেইন এই জ্ঞান ব্যবহার করে সত্যিকারের ডিজিটাল ফিল্টার ডিজাইন শুরু করব — কোন ফ্রিকোয়েন্সি রাখব, কোনটি সরাব, তা নিয়ন্ত্রণ করার হাতিয়ার।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ থ্রেশহোল্ড 0.15 কেন বেছে নেওয়া হলো? যদি এটি 0.03 হতো তাহলে কী ভুল হতে পারত?

কোড আউটপুটে নয়েজ-বিনগুলোর ম্যাগনিটিউড সর্বোচ্চ প্রায় ০.০৮-এর কাছাকাছি (যেমন বিন ১৩-এ ০.০৭৮২)। থ্রেশহোল্ড ০.১৫ এই সর্বোচ্চ নয়েজ মানের প্রায় দ্বিগুণ রেখে একটি নিরাপদ মার্জিন দেয়। যদি থ্রেশহোল্ড ০.০৩ হতো, তাহলে ০.০৭৮২-এর মতো নয়েজ-বিনগুলোও থ্রেশহোল্ড অতিক্রম করত এবং ভুলভাবে "পিক" হিসেবে শনাক্ত হতো (false positive) — অ্যালগরিদম তখন দুটোর বদলে অনেকগুলো ভুয়া ফ্রিকোয়েন্সি রিপোর্ট করত।

প্র ০২ কেন mag শুধু range(N // 2) পর্যন্ত হিসেব করা হয়েছে, পুরো N বিন নয়?

ইনপুট সিগন্যাল x সম্পূর্ণ বাস্তব (real) সংখ্যার — কোনো কাল্পনিক (imaginary) অংশ নেই। M5/L23-এ (DFT-এর বৈশিষ্ট্য) শেখা conjugate-symmetry অনুযায়ী, একটি বাস্তব ইনপুটের DFT-তে X[N-k] সবসময় X[k]-এর কমপ্লেক্স কনজুগেট — অর্থাৎ |X[N-k]| = |X[k]|। তাই বিন N/2-এর পরের অর্ধেক সম্পূর্ণ পুনরাবৃত্তিমূলক (redundant) তথ্য বহন করে, আর শুধু প্রথম অর্ধেক (0 থেকে N/2-1) দেখলেই সম্পূর্ণ, অনন্য (unique) স্পেকট্রাল তথ্য পাওয়া যায়।

প্র ০৩ random.seed(42) লাইনটি সরিয়ে ফেললে কোডের ফলাফলে কী পরিবর্তন হতো?

random.gauss প্রতিবার ভিন্ন এলোমেলো মান তৈরি করত (Python-এর ডিফল্ট র‍্যান্ডম সিড সিস্টেম সময়ের উপর ভিত্তি করে সেট হয়), তাই প্রতিবার Run চাপলে সামান্য ভিন্ন নয়েজ-বিন মান আসত এবং সম্ভবত ভিন্ন ম্যাগনিটিউড সংখ্যা রিপোর্ট হতো (যদিও ১০ Hz আর ৩০ Hz পিক দুটো এত শক্তিশালী যে তারা প্রায় সবসময়ই ধরা পড়ত)। seed(42) ব্যবহার করে আমরা প্রতিবার ঠিক একই নয়েজ পুনরুৎপাদন করি — যা ডিবাগিং ও এই লেসনের প্রতিটি সংখ্যা যাচাইযোগ্য রাখার জন্য জরুরি।

অনুশীলন

  1. চিন্তা করুন: যদি দ্বিতীয় ফ্রিকোয়েন্সি f2 = 30.0-কে f2 = 30.5-এ বদলানো হয় (fs=128, N=128 অপরিবর্তিত, তাই রেজোলিউশন এখনও ১ Hz), তাহলে পিক-ফাইন্ডিং অ্যালগরিদম কোন বিনে (bin) সবচেয়ে বড় ম্যাগনিটিউড দেখাবে বলে আপনার ধারণা, এবং পুনরুদ্ধার করা ফ্রিকোয়েন্সিতে কত ত্রুটি থাকতে পারে?

    ৩০.৫ Hz ঠিক বিন ৩০ (৩০ Hz) আর বিন ৩১ (৩১ Hz)-এর মাঝামাঝি পড়ে — কোনো বিন-কেন্দ্রেই ঠিক পড়ে না। স্পেকট্রাল লিকেজের কারণে (L24) সবচেয়ে কাছের বিনগুলোর একটিতে (৩০ বা ৩১) সবচেয়ে বড় ম্যাগনিটিউড দেখা যাবে, কিন্তু পুনরুদ্ধার করা ফ্রিকোয়েন্সিতে সর্বোচ্চ প্রায় অর্ধেক-বিন (০.৫ Hz) ত্রুটি থাকতে পারে — এই লেসনের ৩ নং সেকশনে ঠিক এই সীমাবদ্ধতা আলোচনা করা হয়েছে।

  2. পরীক্ষা করুন: কোড সেলে f2, a2 = 30.0, 0.6-কে f2, a2 = 30.5, 0.6-এ বদলে দুটো কোড সেল আবার Run করে দেখুন পুনরুদ্ধার করা দ্বিতীয় ফ্রিকোয়েন্সি এবং ত্রুটি কত আসে।

    রান করলে দেখা যাবে সবচেয়ে বড় ম্যাগনিটিউড বিন ৩০ বা ৩১-এর একটিতে থাকবে (নির্দিষ্ট নয়েজের উপর নির্ভর করে, যেহেতু seed=42 এখনও ব্যবহৃত হচ্ছে তাই ফলাফল নির্ধারিত/পুনরুৎপাদনযোগ্য থাকবে), আর পুনরুদ্ধার করা ফ্রিকোয়েন্সি ৩০.৫-এর কাছাকাছি কিন্তু ঠিক তার সমান নয় — ত্রুটি ০-এর বেশি (সর্বোচ্চ ০.৫ Hz পর্যন্ত), প্রশ্ন ১-এর হাতে-করা ধারণার সাথে সঙ্গতিপূর্ণ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
FFT ইমপ্লিমেন্টেশন — বিট-রিভার্সাল ও বাটারফ্লাই