ملاحظة

هذه ترجمة لكتاب Think DSP للمؤلف Allen B. Downey. هذا المحتوى متاح برخصة Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

مقدمة

تُعتبر معالجة الإشارات واحدة من مواضيعي المفضلة. إنها مفيدة في العديد من مجالات العلوم والهندسة، وإذا فهمت الأفكار الأساسية، فإنها توفر رؤى حول العديد من الأشياء التي نراها في العالم، وخاصة الأشياء التي نسمعها.

ولكن، ما لم تكن قد درست الهندسة الكهربائية أو الميكانيكية، فمن المحتمل أن الفرصة لم تتح لك لدراسة معالجة الإشارة. المشكلة هي أن معظم الكتب (والفصول الدراسية التي تستخدمها) تقدم المادة بشكل تدريجي، بدءًا من التجريدات الرياضية مثل phasors. وغالبًا ما تكون المواد نظرية، مع عدد قليل من التطبيقات وقلة من الصلة الظاهرة.

تقوم فرضية هذا الكتاب على أنه إذا كنت تعرف البرمجة، يمكنك استخدام تلك المهارة لتعلم أشياء أخرى، والاستمتاع بذلك.

باستخدام نهج قائم على البرمجة، يمكنني تقديم أهم الأفكار على الفور. بنهاية الفصل الأول، يمكنك تحليل تسجيلات الصوت وإشارات أخرى، وتوليد أصوات جديدة. يقدم كل فصل أسلوبًا جديدًا وتطبيقًا يمكنك تطبيقه على إشارات حقيقية. في كل خطوة، تتعلم كيفية استخدام الأسلوب أولاً، ثم طريقة عمله.

هذا النهج أكثر عملية، وآمل أن تتفق معي على أنه أكثر متعة.

لمن هذا الكتاب؟

الأمثلة والكود الداعم لهذا الكتاب مكتوبة بلغة بايثون. يجب أن تكون على دراية بأساسيات بايثون وأن تألف التعامل مع الكائنات، على الأقل استخدام الكائنات إذا لم تكن تستطيع تعريف كائنات خاصة بك.

إذا لم تكن على دراية بلغة بايثون بالفعل، فقد ترغب في البدء بكتابي الآخر، فكر في بايثون، الذي يعد مقدمة للغة بايثون لمن لا يعرف البرمجة، أو كتاب مارك لوتز تعلم بايثون (Learning Python, Mark Lutz)، الذي قد يناسب ذوي الخبرة في البرمجة بشكل أفضل.

أستخدمُ NumPy وSciPy بشكل واسع. إذا كنت على دراية بهما بالفعل، فهذا رائع، لكنني سأشرح أيضًا الدوال وهياكل البيانات التي أستخدمها.

أفترض أن القارئ يعرف الرياضيات الأساسية، بما في ذلك الأعداد المركبة. لا تحتاج إلى الكثير من التحليل الرياضي؛ يكفي أن تفهم مفاهيم التكامل والتفاضل. أستخدمُ بعض الجبر الخطي، لكنني سأشرحه أثناء تقدمنا.

استخدام الكود

الكود وعينات الصوت المستخدمة في هذا الكتاب متاحة على https://github.com/AllenDowney/ThinkDSP. ‏Git هو نظام إدارة إصدارات يسمح لك بتتبع التعديلات التي تطرأ على ملفات المشروع. تُسمى مجموعة الملفات التي يديرها Git “مستودعًا”. GitHub هو خدمة استضافة توفر تخزينًا لمستودعات Git وواجهة ويب مريحة.

توفر الصفحة الرئيسية لمستودعي على GitHub عدة طرق للعمل مع الكود:

  • يمكنك إنشاء نسخة من مستودعي على GitHub بالضغط على زر Fork. إذا لم يكن لديك حساب على GitHub بالفعل، فستحتاج إلى إنشاء حساب. بعد عملية Fork، سيكون لديك مستودع خاص بك على GitHub يمكنك استخدامه لتتبع الكود الذي تكتبه أثناء العمل على هذا الكتاب. ثم يمكنك استنساخ clone المستودع، مما يعني أنك ستقوم بنسخ الملفات إلى حاسوبك.

  • أو يمكنك استنساخ مستودعي. لا تحتاج إلى حساب على GitHub للقيام بذلك، لكنك لن تتمكن من كتابة تغييراتك مرة أخرى على GitHub.

  • إذا كنت لا تريد استخدام Git على الإطلاق، يمكنك تنزيل الملفات في ملف Zip باستخدام الزر في الزاوية السفلى اليمنى من صفحة GitHub.

كل الكود مكتوب ليعمل في كل من Python 2 و Python 3 دون أي ترجمة.

طورت هذا الكتاب باستخدام Anaconda من Continuum Analytics، وهي توزيعة مجانية من Python تتضمن جميع الحزم التي ستحتاجها لتشغيل الكود (والمزيد). وجدتُ أن Anaconda سهلة التثبيت. بشكل افتراضي، تقوم بعملية تثبيت على مستوى المستخدم، وليس على مستوى النظام، لذا لا تحتاج إلى صلاحيات إدارية. وهي تدعم كل من Python 2 و Python 3. يمكنك تنزيل Anaconda من https://www.anaconda.com/distribution/.

إذا كنت لا تريد استخدام Anaconda، فستحتاج إلى الحزم التالية:

على الرغم من أن هذه الحزم مستخدمة بشكل شائع، إلا أنها ليست مضمنة في جميع تثبيات بايثون، وقد يكون من الصعب تثبيتها في بعض البيئات. إذا واجهت صعوبة في تثبيتها، أوصي باستخدام Anaconda أو إحدى توزيعات بايثون الأخرى التي تتضمن هذه الحزم.

تستخدم معظم التمارين سكريبتات بايثون، ولكن البعض يستخدم أيضًا دفاتر Jupyter. إذا لم تكن قد استخدمت Jupyter من قبل، يمكنك قراءة المزيد عنه على http://jupyter.org.

هناك ثلاث طرق يمكنك من خلالها العمل مع دفاتر Jupyter:

تشغيل Jupyter على جهاز الكمبيوتر الخاص بك إذا قمت بتثبيت Anaconda، فمن المحتمل أنك حصلت على Jupyter بشكل افتراضي. للتحقق، ابدأ الخادم من سطر الأوامر، كما يلي:

    $ jupyter notebook

إذا لم يكن مثبتًا، يمكنك تثبيته في Anaconda كما يلي:

    $ conda install jupyter

عند بدء تشغيل الخادم، يجب أن يفتح متصفح الويب الافتراضي لديك أو ينشئ علامة تبويب جديدة في نافذة متصفح مفتوحة.

تشغيل Jupyter على Binder‏ Binder هو خدمة تشغّل Jupyter في آلة افتراضية. إذا اتبعت هذا الرابط، http://mybinder.org/repo/AllenDowney/ThinkDSP، يجب أن تظهر لك صفحة رئيسية لـ Jupyter تحتوي على دفاتر هذا الكتاب والبيانات المرافقة لها والسكريبتات.

يمكنك تشغيل السكريبتات وتعديلها لتجربة الكود الخاص بك، ولكن الآلة الافتراضية التي تعمل بها مؤقتة. ستختفي أي تغييرات تجريها، جنبًا إلى جنب مع الآلة الافتراضية، إذا تركتها بلا استخدام لأكثر من ساعة تقريبًا.

عرض الدفاتر على nbviewer

عندما نشير إلى دفاتر الملاحظات لاحقًا في الكتاب، سنوفر روابط إلى nbviewer، الذي يوفر عرضًا ثابتًا للكود والنتائج. يمكنك استخدام هذه الروابط لقراءة دفاتر الملاحظات والاستماع إلى الأمثلة، ولكن لا يمكنك تعديل الكود أو تشغيله، أو استخدام الأدوات التفاعلية.

حظًا سعيدًا، واستمتع!

قائمة المساهمين

إذا كان لديك اقتراح أو تصحيح، يرجى إرسال بريد إلكتروني إلى downey@allendowney.com. إذا قمت بإجراء تغيير بناءً على ملاحظاتك، سأضيفك إلى قائمة المساهمين (ما لم تطلب أن يتم استثناؤك).

إذا قمت بتضمين جزء على الأقل من الجملة التي يظهر فيها الخطأ، فإن ذلك يسهل علي البحث. أرقام الصفحات والأقسام جيدة أيضًا، ولكن ليست سهلة للعمل بها. شكرًا!

  • قبل أن أبدأ الكتابة، كانت أفكاري حول هذا الكتاب تستفيد من المحادثات مع Boulos Harb a في جوجل وAurelio Ramos، الذي كان سابقًا في Harmonix Music Systems.

  • خلال فصل الخريف 2013، عمل Nathan Lintz و Ian Daniher معي على مشروع دراسة مستقلة وساعداني في المسودة الأولى من هذا الكتاب.

  • في منتدى DSP على ريديت، ساعدني المستخدم المجهول RamjetSoundwave في إصلاح مشكلة في تنفيذني للضجيج البراوني. ووجد andodli خطأً مطبعيًا.

  • في ربيع 2015، كان لي شرف تدريس هذا المحتوى مع البروفيسور Oscar Mur-Miranda والبروفيسور Siddhartan Govindasamy. كلاهما قدم العديد من الاقتراحات والتصحيحات.

  • صحح Silas Gyger خطأً حسابياً.

  • أرسل Giuseppe Masetti عددًا من الاقتراحات المفيدة جداً.

  • أرسل Eric Peters العديد من الاقتراحات المفيدة.

شكر خاص لموقع Freesound، الذي يعد مصدرًا للعديد من عينات الصوت التي استخدمتها في هذا الكتاب، ولمستخدمي Freesound الذين رفعوا تلك الأصوات. أدرجتُ بعض ملفاتهم في مستودع GitHub لهذا الكتاب، باستخدام أسماء الملفات الأصلية، لذا يجب أن يكون من السهل العثور على مصادرهم.

لسوء الحظ، فإن معظم مستخدمي Freesound لا يقدمون أسمائهم الحقيقية، لذلك يمكنني فقط شكرهم باستخدام أسماء المستخدمين الخاصة بهم. تم تقديم العينات المستخدمة في هذا الكتاب من قبل مستخدمي Freesound: ‏iluppai، ‏wcfl10، ‏thirsk، ‏docquesting، ‏kleeb، ‏landup، ‏zippi1، ‏themusicalnomad، ‏bcjordan،‏ rockwehrmann،‏ marcgascon7،‏ jcveliz. شكرًا لكم جميعًا!

::: جدول المحتويات :::

الأصوات والإشارات

تمثل الإشارة signal كمية متغيّرة مع الزمن. هذا التعريف يعتبر تجريديًا جداً، لذا دعونا نبدأ بمثال ملموس: الصوت. الصوت هو تباين في ضغط الهواء. تمثل إشارة الصوت التغيّرات في ضغط الهواء على مر الزمن.

الميكروفون هو جهاز يقيس هذه التغيّرات ويولد إشارة كهربائية تمثل الصوت. ومكبر الصوت هو جهاز يأخذ إشارة كهربائية وينتج صوتًا. تُسمى الميكروفونات ومكبرات الصوت محولات transducers لأنها تحول الإشارات من شكل إلى آخر.

يتناول هذا الكتاب معالجة الإشارات، والتي تشمل العمليات الخاصة بتوليد وتحويل وتحليل الإشارات. سأركز على إشارات الصوت، لكن نفس الأساليب تنطبق على الإشارات الإلكترونية، والاهتزازات الميكانيكية، والإشارات في مجالات أخرى عديدة.

كما أنها تنطبق على الإشارات التي تتغير في الفضاء بدلاً من الزمن، مثل تغيّر الارتفاع عن سطح البحر على طريق جبلي. وتنطبق أيضًا على الإشارات متعددة الأبعاد، مثل الصور، التي يمكنك اعتبارها كإشارة تتغير في فضاء ثنائي الأبعاد. أو الفيديو، وهو إشارة تتغيّر في فضاء ثنائي الأبعاد و الزمن.

لكننا نبدأ بإشارات الصوت أحادية البعد البسيطة.

الكود الخاص بهذا الفصل موجود في chap01.ipynb، والموجود في المستودع الخاص بهذا الكتاب (انظر القسم 1.2{reference-type=“ref” reference=“code”}). يمكنك أيضًا رؤيته على http://tinyurl.com/thinkdsp01.

الإشارات الدورية

مقطع من تسجيل صوت جرس.{#fig.sounds1 height=“2.5in”}

سنبدأ بـ الإشارات الدورية periodic signals، وهي الإشارات التي تتكرر بعد فترة زمنية معينة. على سبيل المثال، إذا ضربت جرسًا، فإنه يهتز وينتج صوتًا. إذا سجّلتَ ذلك الصوت ورسمتَ الإشارة المحوّلة، ستبدو مثل الشكل 2.1{reference-type=“ref” reference=“fig.sounds1”}.

تشبه هذه الإشارة الدالة الجيبية sinusoid، يعني أن لها نفس شكل الدالة الجيبية المثلثية.

يمكنك أن ترى أن هذه الإشارة دورية. لقد اخترت المدة الزمنية لإظهار ثلاث تكرارات كاملة، والمعروفة أيضًا باسم الدورات cycles. مدة كل تكرار، التي تُسمى الدور period، تبلغ حوالي 2.3 مللي ثانية.

تردد frequency الإشارة هو عدد الدورات في الثانية، وهو مقلوب الدور. واحدة التردد هي دورات في الثانية، أو هرتز، واختصاراً “Hz”. (بشكل أدق، إن عدد الدورات هو عدد بلا أبعاد، لذا فإن الهرتز يعادل في الحقيقة كلمة “لكل ثانية”).

تردد هذه الإشارة حوالي 439 هرتز، وهو أقل بقليل من 440 هرتز، وهي نغمة الضبط القياسية للموسيقى الأوركسترالية. الاسم الموسيقي لهذه النغمة هو A، أو بشكل أكثر تحديدًا، A4. إذا لم تكن معتادًا على “التدوين العلمي للنغمات”، فإن اللاحقة الرقمية تشير إلى أي أوكتاف تقع فيه النغمة. A4 هي النغمة A فوق نغمة C الوسطى. أما A5 فهي تقع في أوكتاف واحد أعلى. انظر http://en.wikipedia.org/wiki/Scientific_pitch_notation.

مقطع من تسجيل لآلة كمان.{#fig.sounds2 height=“2.5in”}

تولد الشوكة الرنانة موجة جيبية لأن اهتزاز الشُّعَب هو شكل من أشكال الحركة التوافقية البسيطة. تنتج معظم الآلات الموسيقية إشارات دورية، لكن شكل هذه الإشارات ليس جيبيًا. على سبيل المثال، يُظهر الشكل 2.2{reference-type=“ref” reference=“fig.sounds2”} مقطعًا من تسجيل لآلة كمان تعزف رباعية بوتشيريني الوترية رقم 5 في مي، الحركة الثالثة.

مرة أخرى، يمكننا أن نرى أن الإشارة دورية، لكن شكل الإشارة أكثر تعقيدًا. يُطلق على شكل الإشارة الدورية اسم شكل الموجة. تنتج معظم الآلات الموسيقية أشكال موجات صوتية أكثر تعقيدًا من الموجة الجيبية. يحدد شكل الموجة الصوتية اللون الصوتي timbre، وهو إدراكنا لجودة الصوت. عادةً ما يشعر الناس أن الموجات الصوتية المعقدة غنية ودافئة وأكثر إثارة للاهتمام من الموجات الجيبية.

التحليل الطيفي

طيف مقطع من تسجيل الكمان.{#fig.sounds3 height=“2.5in”}

الموضوع الأكثر أهمية في هذا الكتاب هو التفريق الطيفي spectral decomposition، وهو الفكرة التي تقول إن أي إشارة يمكن التعبير عنها كمجموع من الموجات الجيبية بترددات مختلفة.

وأهم فكرة رياضية في هذا الكتاب هي تحويل فورييه المتقطع discrete Fourier transform، أو DFT، الذي يأخذ إشارة وينتج طيفها. الطيف هو مجموعة الموجات الجيبية التي تتجمع لتنتج الإشارة.

وأهم خوارزمية في هذا الكتاب هي تحويل فورييه السريع Fast Fourier transform، أو FFT، وهي طريقة فعالة لحساب DFT.

على سبيل المثال، يُظهر الشكل 2.3{reference-type=“ref” reference=“fig.sounds3”} طيف تسجيل الكمان في الشكل 2.2{reference-type=“ref” reference=“fig.sounds2”}. المحور السيني هو نطاق الترددات التي تشكل الإشارة. والمحور الصادي يُظهر القوة أو المطال amplitude لكل مكون ترددي.

أدنى مكون ترددي يُسمى التردد الأساسي fundamental frequency. التردد الأساسي لهذه الإشارة قريب من 440 هرتز (في الواقع أقل قليلاً، أو “مستوي flat”).

في هذه الإشارة، يمتلك التردد الأساسي أكبر مطال، لذا فهو أيضًا التردد المسيطر dominant frequency. عادةً ما يتم تحديد نغمة الصوت المدركة من خلال التردد الأساسي، حتى لو لم يكن هو المسيطر.

توجد spikes أخرى في الطيف عند الترددات 880 و1320 و1760 و2200، وهي مضاعفات صحيحة للتردد الأساسي. تُسمى هذه المكونات التوافقيات harmonics لأنها متوافقة موسيقيًا مع التردد الأساسي:

  • 880 هو تردد A5، وهو أوكتاف واحد أعلى من التردد الأساسي. الأوكتاف octave هو مضاعفة في التردد.

  • 1320 هو تقريبًا E6، وهو خامس مثالي فوق A5. إذا لم تكن على دراية بالفواصل الموسيقية مثل “الخامس المثالي”، انظر https://en.wikipedia.org/wiki/Interval_(music).

  • 1760 هو A6، وهو أوكتافان أعلى من التردد الأساسي.

  • 2200 هو تقريبًا C♯7، وهو ثالث كبير فوق A6.

تشكل هذه التوافقيات نغمات وتر A الكبير، على الرغم من أنها ليست جميعها في نفس الأوكتاف. بعض منها تقريبية فقط لأن النغمات التي تشكل الموسيقى الغربية قد تم تعديلها لتناسب التماثل المتساوي/التساوي في التردد/(غوغل: المزاج المتساوي) (انظر http://en.wikipedia.org/wiki/Equal_temperament).

بالنظر إلى التوافقيات ومطالاتها، يمكنك إعادة بناء الإشارة عن طريق جمع الإشارات الجيبية. سنرى ذلك في القسم التالي.

الإشارات

كتبتُ وحدة بايثون تسمى thinkdsp.py تحتوي على فئات ودوال للعمل مع الإشارات والطيف[^1]. ستجدها في المستودع الخاص بهذا الكتاب (انظر القسم 1.2{reference-type=“ref” reference=“code”}).

لتمثيل الإشارات، توفر thinkdsp فئة تسمى Signal، وهي الفئة الأم لعدة أنواع من الإشارات، بما في ذلك Sinusoid، التي تمثل كلاً من إشارات الجيب sine والتجيب (تمام الجيب cosine).

تقدم thinkdsp دوالاً لإنشاء إشارات جيبية وتجيبية:

    cos_sig = thinkdsp.CosSignal(freq=440, amp=1.0, offset=0)
    sin_sig = thinkdsp.SinSignal(freq=880, amp=0.5, offset=0)

freq هو التردد بواحدة الهرتز. amp هو المطال بوحدات غير محددة حيث يتم تعريف 1.0 كأكبر مطال يمكننا تسجيله أو تشغيله.

offset هو إزاحة الطور phase offset بواحدة الراديان. تحدد إزاحة الطور مكان بدء الإشارة في الدورة. على سبيل المثال، تبدأ إشارة الجيب مع offset=0 عند ، وهو 0. مع offset=pi/2، تبدأ عند ، وهو 1.

تحتوي الإشارات على دالة __add__، لذا يمكنك استخدام عامل + لجمعها:

    mix = sin_sig + cos_sig

النتيجة هي SumSignal، التي تُمثِّل مجموع إشارتين أو أكثر.

الإشارة Signal هي في الأساس تمثيل بلغة بايثون لدالة رياضية. معظم الإشارات مُعرَّفة لجميع قيم t، من سالب مالانهاية إلى موجب مالانهاية.

لا يمكنك القيام بالكثير مع Signal حتى تقوم بتقييمها. في هذا السياق، يعني “التقييم” أخذ تسلسل من النقاط الزمنية، ts، وحساب القيم المقابلة للإشارة، ys. لقد مثّلتُ ts و ys باستخدام مصفوفات NumPy وغلفتها في كائن يُسمى Wave.

تمثل Wave إشارة تم تقييمها عند تسلسل من النقاط الزمنية. تُسمى كل نقطة زمنية إطار frame (مصطلح مُستعار من الأفلام والفيديو). يُطلق على القياس نفسه عينة sample، على الرغم من أنه يُستخدم أحيانًا مصطلح “إطار” و”عينة” بالتبادل.

يوفر Signal دالة make_wave، التي تُرجع كائن Wave جديد:

    wave = mix.make_wave(duration=0.5, start=0, framerate=11025)

duration هي طول Wave بالثواني. start هو وقت البدء، أيضًا بالثواني. framerate هو العدد (الصحيح) من الإطارات في الثانية، وهو أيضًا عدد العينات في الثانية.

11,025 إطارًا في الثانية هو أحد عدة معدّلات إطار تُستخدم عادةً في صيغ ملفات الصوت، ومنها صيغة Waveform Audio File (WAV) وmp3.

هذا المثال يقيم الإشارة من t=0 إلى t=0.5 عند 5,513 إطارًا متساوي المسافة (لأن 5,513 هو نصف 11,025). الوقت بين الإطارات، أو خطوة الزمن timestep، هو 1/11025 ثانية، حوالي 91 μs (ميكروثانية).

تقدم فئة Wave دالة plot التي تستخدم pyplot. يمكنك رسم الموجة بهذه الدالة:

    wave.plot()
    pyplot.show()

pyplot هو جزء من matplotlib; وهو مدرج في العديد من توزيعات Python، أو قد تحتاج إلى تثبيته.

جزء من مزيج من إشارتين جيبيتين.{#fig.sounds4 height=“2.5in”}

عندما يكون freq=440، هناك 220 دورة كل 0.5 ثانية، لذا سيبدو هذا الرسم البياني ككتلة صلبة من اللون. لتكبير الصورة على عدد قليل من الدورات، يمكننا استخدام segment، الذي ينسخ جزءًا من كائن الموجة Wave ويعيد موجة جديدة:

    period = mix.period
    segment = wave.segment(start=0, duration=period*3)

period هي خاصية من خصائص الإشارة Signal؛ وهي تعطي قيمة الدور بالثواني.

start و duration هما بالثواني. هذا المثال ينسخ الدورات الثلاثة الأولى من mix. النتيجة هي كائن Wave.

إذا قمنا برسم segment، ستبدو كما في الشكل 2.4{reference-type=“ref” reference=“fig.sounds4”}. تحتوي هذه الإشارة على مكونين تردديين، لذا فهي أكثر تعقيدًا من إشارة الشوكة الرنانة، ولكنها أقل تعقيدًا من صوت الكمان.

قراءة وكتابة الموجات

تقدم thinkdsp دالة read_wave، التي تقرأ ملف WAV وتعيد موجة:

    violin_wave = thinkdsp.read_wave('input.wav')

وتقدم Wave دالة write، التي تكتب ملف WAV:

    wave.write(filename='output.wav')

يمكنك الاستماع إلى الموجة باستخدام أي مشغل وسائط يدعم ملفات WAV. على أنظمة UNIX، أستخدم aplay، الذي يتميز بالبساطة والموثوقية، وهو متضمن في العديد من توزيعات Linux.

تقدم thinkdsp أيضًا دالة play_wave، التي تشغل مشغل الوسائط كعملية فرعية:

    thinkdsp.play_wave(filename='output.wav', player='aplay')

تستخدم aplay بشكل افتراضي، ولكن يمكنك توفير اسم مشغل آخر.

الطيف

توفر Wave دالة make_spectrum، التي تعيد طيفًا Spectrum:

    spectrum = wave.make_spectrum()

وتوفر Spectrum دالة plot:

    spectrum.plot()

تقدم Spectrum ثلاث طرق لتعديل الطيف:

  • low_pass تطبق فلتر تمرير منخفض، مما يعني أن المكونات التي تتجاوز تردد القطع المعين يتم توهينها (أي تقليل مقدارها) بعامل ما.

  • high_pass تطبق فلتر تمرير مرتفع، مما يعني أنها توهن المكونات التي تقل عن تردد القطع.

  • band_stop توهن المكونات التي تقع في النطاق بين ترددي القطع.

هذا المثال يوهن جميع الترددات التي تتجاوز 600 بنسبة 99%:

   spectrum.low_pass(cutoff=600, factor=0.01)

فلتر التمرير المنخفض يزيل الأصوات الساطعة bright عالية التردد، لذا فإن النتيجة تبدو مكتومة وداكنة. للاستماع إلى النتيجة، يمكنك تحويل Spectrum مرة أخرى إلى Wave، ثم تشغيلها.

    wave = spectrum.make_wave()
    wave.play('temp.wav')

تكتب دالة play الموجة إلى ملف ثم تشغّله. إذا كنت تستخدم دفاتر Jupyter، يمكنك استخدام make_audio، الذي يصنع عنصر صوتي يشغّل الصوت.

كائنات الموجة Wave

العلاقات بين الفئات في
thinkdsp.{#fig.diagram1 width=“3.5in”}

لا يوجد شيء معقد جدًا في thinkdsp.py. معظم الدوال التي يوفرها هي أغلفة رقيقة حول الدوال من NumPy وSciPy.

الفئات الرئيسية في thinkdsp هي الإشارة Signal، الموجة Wave، والطيف Spectrum. بناءً على إشارة، يمكنك صنع موجة. بناءً على موجة، يمكنك صنع طيف، ومن الطيف يمكنك صنع موجة مرة أخرى. هذه العلاقات موضحة في الشكل 2.5{reference-type=“ref” reference=“fig.diagram1”}.

يحتوي كائن الموجة على ثلاث صفات: ys هي مصفوفة NumPy تحتوي على القيم في الإشارة؛ ts هي مصفوفة الأوقات التي تم تقييم الإشارة فيها أو أخذ عينات منها؛ وframerate هو عدد العينات لكل وحدة زمنية. واحدة قياس الزمن هي الثانية عادةً، لكن يمكن استخدام واحدة أخرى. في أحد الأمثلة، استخدمتُ الأيام كواحدة زمنية.

توفر الموجة Wave أيضًا ثلاث خصائص للقراءة فقط: start، end، وduration. إذا عدلت مصفوفة ts، تتغير هذه الخصائص وفقًا لذلك.

لتعديل موجة، يمكنك الوصول إلى ts و ys مباشرة. على سبيل المثال:

wave.ys *= 2
wave.ts += 1

السطر الأول يزيد مطال الموجة بمقدار 2، ما يجعلها أعلى صوتاً. السطر الثاني يزيح الموجة في الزمن، ما يجعلها تبدأ بعد 1 ثانية.

لكن Wave يوفر دوالاً تؤدي العديد من العمليات الشائعة. على سبيل المثال، يمكن كتابة التحويلين السابقين كما يلي:

wave.scale(2)
wave.shift(1)

يمكنك قراءة الوثائق الخاصة بهذه الدوال وغيرها على http://greenteapress.com/thinkdsp.html.

كائنات الإشارة Signal

Signal هي فئة أساسية توفر دوالاً مشتركة بين جميع أنواع الإشارات، مثل make_wave. الفئات الفرعية ترث هذه الدوال وتوفر evaluate، التي تقيّم الإشارة عند سلسلة محددة من الأوقات.

على سبيل المثال، Sinusoid هي فئة فرعية من Signal، مع هذا التعريف:

class Sinusoid(Signal):

    def __init__(self, freq=440, amp=1.0, offset=0, func=np.sin):
        Signal.__init__(self)
        self.freq = freq
        self.amp = amp
        self.offset = offset
        self.func = func

معاملات __init__ هي:

  • freq: التردد بالدورات في الثانية، أو هرتز.

  • amp: المطال. واحدة قياس المطال عشوائية، وغالباً ما يتم اختيارها بحيث تقابل قيمة 1.0 مع الحد الأقصى للإدخال من الميكروفون أو الحد الأقصى للإخراج إلى مكبر الصوت.

  • offset: يشير إلى المكان الذي تبدأ فيه الإشارة في دورها؛ يقاس offset بواحدة الراديان، للأسباب التي سأشرحها أدناه.

  • func: دالة بايثون تُستخدم لتقييم الإشارة عند نقطة زمنية معينة. عادةً ما تكون np.sin أو np.cos، مما ينتج إشارة جيبية أو تجيبية.

مثل العديد من دوال التهيئة، تخزّن هذه الدالة المعاملات لاستخدامها في المستقبل.

توفر Signal دالة make_wave، والتي تبدو كما يلي:

    def make_wave(self, duration=1, start=0, framerate=11025):
        n = round(duration * framerate)
        ts = start + np.arange(n) / framerate
        ys = self.evaluate(ts)
        return Wave(ys, ts, framerate=framerate)

start و duration هما وقت البدء والمدة بالثواني. framerate هو عدد الإطارات (العينات) في الثانية.

n هو عدد العينات، و ts هو مصفوفة NumPy تحوي أوقات أخذ العينات.

لحساب ys، تستدعي make_wave دالة evaluate، التي توفرها Sinusoid:

    def evaluate(self, ts):
        phases = PI2 * self.freq * ts + self.offset
        ys = self.amp * self.func(phases)
        return ys

دعونا نفكك هذه الدالة خطوة بخطوة:

  1. self.freq هي التردد بالدورات في الثانية، وكل عنصر من ts هو وقت بالثواني، لذا فإن حاصل ضربهما هو عدد الدورات منذ وقت البدء.

  2. PI2 هو ثابت يحوي قيمة 2π. الضرب في PI2 يحول من الدورات إلى الطور phase. يمكنك اعتبار الطور على أنه “الدورات منذ وقت البدء” معبراً عنها بالراديان. كل دورة تساوي 2π راديان.

  3. self.offset هي الطور عندما يكون t يساوي ts[0]. هذه القيمة تحرّك الإشارة يسارًا أو يمينًا في الزمن.

  4. إذا كانت self.func هي np.sin أو np.cos، فإن النتيجة هي قيمة بين -1 و +1.

  5. الضرب في self.amp يعطي إشارة تتراوح ما بين -self.amp إلى +self.amp.

في التدوين الرياضي، تُكتب evaluate على النحو التالي:

حيث A هو المطال، و f هو التردد، و t هو الزمن، و هو إزاحة الطور. قد يبدو كأنني كتبت الكثير من الشيفرات لتقييم تعبير بسيط واحد، ولكن كما سنرى، توفر هذه الشيفرات إطارًا للتعامل مع جميع أنواع الإشارات، وليس فقط الجيبية.

تمارين

قبل أن تبدأ هذه التمارين، يجب عليك تنزيل الشيفرة الخاصة بهذا الكتاب، وفقًا للتعليمات في القسم 1.2{reference-type=“ref” reference=“code”}.

حلول هذه التمارين موجودة في chap01soln.ipynb.

::: تمرين تمرين 2.1 إذا كان لديك Jupyter، قم بتحميل chap01.ipynb، اقرأ محتوياته، وشغّل الأمثلة. يمكنك أيضًا عرض هذه الدفتر على http://tinyurl.com/thinkdsp01. :::

::: تمرين تمرين 2.2. اذهب إلى http://freesound.org ونزل عينة صوتية تتضمن موسيقى، أو كلام، أو أصوات أخرى ذات نغمة (درجة صوتية pitch) محددة بشكل جيد. اختر جزءًا طوله حوالي نصف ثانية حيث تكون النغمة ثابتة. احسب وارسم طيف الجزء الذي اخترته. ما العلاقة التي تستنتجها بين لون الصوت timbre والهيكل التوافقي harmonic structure الذي تراه في الطيف؟

استخدم high_pass و low_pass و band_stop لتصفية بعض التوافقيات. ثم قم بتحويل الطيف مرة أخرى إلى موجة واستمع إليها. كيف يرتبط الصوت بالتغييرات التي أجريتها في الطيف؟

::: تمرين تمرين 2.3. قم بتوليف synthesize إشارة مركبة من خلال إنشاء كائنات SinSignal و CosSignal وجمعها معاً. احسب قيم الإشارة للحصول على Wave، واستمع إليها. احسب طيفها ورسمه. ماذا يحدث إذا أضفت مكونات ترددية ليست مضاعفات للعنصر الأساسي؟ :::

::: تمرين تمرين 2.4. اكتب دالة تسمى stretch تأخذ Wave وعامل تمديد تسرع أو تبطئ الموجة عن طريق تعديل ts و framerate. تلميح: تحتاج أن تكتب سطرين كود فقط. :::

الأنغام

في هذا الفصل، أستعرض عدة أشكال موجية جديدة؛ سننظر إلى أطيافها لفهم البنية النغمية harmonic structure لها، وهي مجموعة الإشارات الجبيبة sinusoids التي تتكون منها.

سأقدم أيضًا أحد أهم الظواهر في معالجة الإشارات الرقمية: التعرّج aliasing. وسأشرح قليلاً عن كيفية عمل فئة Spectrum.

الكود الخاص بهذا الفصل موجود في chap02.ipynb في مستودع هذا الكتاب (انظر القسم 1.2{reference-type=“ref” reference=“code”}). يمكنك أيضًا مشاهدته على http://tinyurl.com/thinkdsp02.

الموجات المثلثية

تحتوي الموجة الجيبية على مكون ترددي واحد فقط، لذا فإن طيفها يحتوي على قمة واحدة فقط. أما أشكال الموجات الأكثر تعقيدًا، مثل تسجيل الكمان، فينتج عنها تحويل فورييه المتقطع (DFT) يحتوي على العديد من القمم. في هذا القسم، نستكشف العلاقة بين الموجات وأطيافها.

جزء من إشارة مثلثية عند 200 هرتز.{#fig.triangle.200.1 height=“2.5in”}

سأبدأ بموجة مثلثية، والتي تبدو كأنها موجة جيبية مرسومة بخطوط مستقيمة. يظهر الشكل 3.1{reference-type=“ref” reference=“fig.triangle.200.1”} موجة مثلثية بتردد 200 هرتز.

لإنشاء موجة مثلثية، يمكنك استخدام thinkdsp.TriangleSignal:

class TriangleSignal(Sinusoid):

    def evaluate(self, ts):
        cycles = self.freq * ts + self.offset / PI2
        frac, _ = np.modf(cycles)
        ys = np.abs(frac - 0.5)
        ys = normalize(unbias(ys), self.amp)
        return ys

ترث TriangleSignal دالة __init__ من Sinusoid، لذا تأخذ نفس المعاملات: freq وamp وoffset.

الفرق الوحيد هو في evaluate. كما رأينا سابقًا، ts هي سلسلة من أوقات العينات التي نريد تقييم الإشارة فيها.

هناك العديد من الطرق لإنشاء موجة مثلثية. التفاصيل ليست مهمة، لكن إليك كيف تعمل evaluate:

  1. cycles هو عدد الدورات منذ وقت البدء. تقسم دالة np.modf عدد الدورات إلى الجزء الكسري، المخزن في frac، والجزء الصحيح، الذي يتم تجاهله [^2].

  2. frac هو تسلسل يتزايد من 0 إلى 1 وفق التردد المعطى. بطرح 0.5 نحصل على قيم بين -0.5 و 0.5. وبأخذ القيمة المطلقة تنتج موجة تتراوح بين 0.5 و 0.

  3. unbias ينقل الموجة للأسفل بحيث يكون مركزها عند 0؛ ثم تغيّر دالة normalize حجمها لتتناسب مع المطال المطلوب، amp.

إليك الكود الذي ينتج الشكل شكل 3.1{reference-type=“ref” reference=“fig.triangle.200.1”}:

signal = thinkdsp.TriangleSignal(200)
signal.plot()

طيف إشارة مثلثية عند 200 هرتز، معروض على مقياسين عموديين. النسخة على اليمين تقطع الأساس لتظهر التوافقيات بشكل أوضح.{#fig.triangle.200.2 height=“2.5in”}

بعد ذلك يمكننا استخدام Signal لصنع Wave، واستخدام Wave لصنع Spectrum:

wave = signal.make_wave(duration=0.5, framerate=10000)
spectrum = wave.make_spectrum()
spectrum.plot()

شكل 3.2{reference-type=“ref” reference=“fig.triangle.200.2”} فيه مخرجين للنتيجة؛ المظهر على اليمين مُعدل لتبدو فيه المتناغمات بشكل أوضح. كما هو متوقع، فإن أعلى قمة تكون عند التردد الأساسي، 200 هرتز، وهناك قمم إضافية عند الترددات التناغمية، وهي المضاعفات الصحيحة للتردد 200.

لكن المفاجأة هي أنه لا توجد قمم عند المضاعفات الزوجية: 400، 800، إلخ. متناغمات الأمواج المثلثية جميعها مضاعفات فردية للتردد الأساسي، وهي في هذا المثال 600، 1000، 1400، إلخ.

ميزة أخرى في هذا الطيف هي العلاقة بين المطال وتردد المتناغمات. تنقص مطال الترددات المتناغمة بصورة متناسبة مع تربيع التردد. على سبيل المثال، نسبة تردد أول تناغمين (200 و600 هرتز) هي 3، ونسبة المطال هي تقريبًا 9. نسبة تردد التناغمين التاليين (600 و1000 هرتز) هي 1.7، ونسبة المطال هي تقريبًا . تُسمى هذه العلاقة البنية التناغمية harmonic structure.

الموجات المربعة

جزء من إشارة مربعة عند 100
هرتز.{#fig.square.100.1 height=“2.5in”}

thinkdsp يوفر أيضًا SquareSignal، والذي يمثل إشارة مربعة. إليك تعريف الفئة:

    class SquareSignal(Sinusoid):
 
        def evaluate(self, ts):
            cycles = self.freq * ts + self.offset / PI2
            frac, _ = np.modf(cycles)
            ys = self.amp * np.sign(unbias(frac))
            return ys

بنفس طريقة TriangleSignal، ترث فئة SquareSignal دالة __init__ من Sinusoid، لذا تأخذ نفس المعاملات.

ودالة evaluate مشابهة أيضًا. مرة أخرى، cycles هو عدد الدورات منذ وقت البدء، وfrac هو الجزء الكسري، الذي يتزايد من 0 إلى 1 في كل دورة.

unbias يحول frac بحيث يتدرج من -0.5 إلى 0.5، ثم تقوم np.sign باستبدال كل القيم السلبية بالقيمة -1 والقيم الإيجابية ب 1. وبالضرب بالمطال amp تنتج لدينا موجة مربعة تقفز ما بين -amp و amp.

طيف إشارة مربعة عند 100 هرتز.{#fig.square.100.2 height=“2.5in”}

يظهر الشكل 3.3{reference-type=“ref” reference=“fig.square.100.1”} ثلاث دورات من موجة مربعة بتردد 100 هرتز، بينما يظهر الشكل 3.4{reference-type=“ref” reference=“fig.square.100.2”} طيفها.

مثل الموجة المثلثية، تحتوي الموجة المربعة على متناغمات فردية فقط، وهذا هو السبب في وجود قمم عند 300 و500 و700 هرتز، وما إلى ذلك. لكن مطال المتناغمات ينخفض بشكل أبطأ. بشكل محدد، ينخفض المطال بصورة تتناسب مع التردد (وليس تربيع التردد).

تتيح لك التمارين في نهاية هذا الفصل فرصة لاستكشاف أشكال موجية أخرى وبنى تناغميّة أخرى.

التداخل

طيف إشارة مثلثية عند 1100 هرتز تم أخذ عينات منها بمعدل 10,000 إطار في الثانية. العرض على اليمين مقاس ليظهر التوافقيات.{#fig.triangle.1100.2 height=“2.5in”}

لدي اعتراف. لقد اخترت الأمثلة في القسم السابق بعناية لتجنب مناقشة فكرة مربكة. لكن آن الأوان حتى نرتبك الآن.

الشكل 3.5{reference-type=“ref” reference=“fig.triangle.1100.2”} يظهر طيف موجة مثلثية عند 1100 هرتز، تم أخذ عينات منها بمعدل 10,000 لقطة في الثانية. مرة أخرى، العرض على اليمين مُعدل حجمه ليظهر المتناغمات.

يجب أن تكون متناغمات هذه الموجة عند 3300، 5500، 7700، و9900 هرتز. في الشكل، هناك قمم عند 1100 و3300 هرتز، كما هو متوقع، لكن القمة الثالثة هي عند 4500، وليس 5500 هرتز. القمة الرابعة هي عند 2300، وليس 7700 هرتز. وإذا نظرت عن كثب، فإن القمة التي ينبغي أن تكون عند 9900 هي في الواقع عند 100 هرتز. ما الذي يحدث؟

المشكلة هي أنه عندما تقوم بتقييم الإشارة عند لحظات زمنية متقطعة، تضيع المعلومات ما بين العينات. بالنسبة للمكونات ذات التردد المنخفض، لا تظهر أي مشكلة، لأن لديك الكثير من العينات لكل دورة.

لكن إذا أخذت عينات من إشارة عند 5000 هرتز بمعدل 10,000 إطار في الثانية، فإنك تملك عينتان فقط من كل دورة. هذا العدد من العينات يكفي بالكاد، لكن إذا كان التردد أعلى، فهو غير كافٍ.

لمعرفة السبب، دعونا نولد إشارات تجيبية عند 4500 و5500 هرتز، ونأخذ عينات منها بمعدل 10,000 إطار في الثانية:

        framerate = 10000
 
        signal = thinkdsp.CosSignal(4500)
        duration = signal.period*5
        segment = signal.make_wave(duration, framerate=framerate)
        segment.plot()
 
        signal = thinkdsp.CosSignal(5500)
        segment = signal.make_wave(duration, framerate=framerate)
        segment.plot()

إشارات تجيبية عند تردد 4500 و5500 هرتز، تم أخذ عينات منها بمعدل 10,000 إطار في الثانية. الإشارات مختلفة، لكن العينات متطابقة.{#fig.aliasing1 height=“3.5in”}

تظهر النتيجة في الشكل 3.6{reference-type=“ref” reference=“fig.aliasing1”}. قمت برسم الإشارات بخطوط رمادية رفيعة والعينات باستخدام خطوط عمودية، لتسهيل المقارنة بين الموجتين. يجب أن تكون المشكلة واضحة: على الرغم من أن الإشارات مختلفة، إلا أن الموجات متطابقة!

عندما نأخذ عينات من إشارة بتردد 5500 هرتز بمعدل 10,000 إطار في الثانية، تكون النتيجة غير قابلة للتمييز عن إشارة بتردد 4500 هرتز. لنفس السبب، فإن إشارة بتردد 7700 هرتز غير قابلة للتمييز عن 2300 هرتز، وإشارة بتردد 9900 هرتز غير قابلة للتمييز عن 100 هرتز.

تُسمى هذه الظاهرة aliasing لأن الإشارة ذات التردد العالي عند أخذ عينات منها، تبدو وكأنها إشارة ذات تردد منخفض.

في هذا المثال، فإن أعلى تردد يمكن قياسه هو 5000 هرتز، وهو نصف معدل أخذ العينات. الترددات التي تزيد عن 5000 هرتز تُطوى تحت 5000 هرتز، ولهذا السبب يُطلق على هذا العتبة أحيانًا اسم “تردد الطي folding frequency”. كما يُطلق عليه أحيانًا تردد نيكويست Nyquist frequency. انظر http://en.wikipedia.org/wiki/Nyquist_frequency.

يستمر نمط الطي إذا انخفض التردد البديل aliased frequency تحت الصفر. على سبيل المثال، المتناغم الخامس لموجة مثلثية بتردد 1100 هرتز يكون عند 12,100 هرتز. عند طيّه عند 5000 هرتز، سيظهر عند -2100 هرتز، لكنه يطوى مرة أخرى عند 0 هرتز، ليعود إلى 2100 هرتز. في الواقع، يمكنك رؤية قمة صغيرة عند 2100 هرتز في الشكل 3.4{reference-type=“ref” reference=“fig.square.100.2”}، والقمة التالية عند 4300 هرتز.

حساب الطيف

لقد رأينا دالة make_spectrum في فئة Wave عدة مرات. إليك محتوياتها (مع ترك بعض التفاصيل التي سنتناولها لاحقًا):

from np.fft import rfft, rfftfreq
 
# class Wave:
    def make_spectrum(self):
        n = len(self.ys)
        d = 1 / self.framerate
 
        hs = rfft(self.ys)
        fs = rfftfreq(n, d)
 
        return Spectrum(hs, fs, self.framerate)

المعامل self هو كائن من نوع Wave.‏ n هو عدد العينات في الموجة، وd هو مقلوب معدل الإلتقاط، أي الزمن الفاصل بين العينات.

np.fft هي وحدة NumPy التي توفر وظائف مرتبطة بـ تحويل فورييه السريع (FFT)، وهي خوارزمية فعالة تحسب تحويل فورييه المتقطع (DFT).

make_spectrum يستخدم rfft، والذي يعني “تحويل فورييه السريع الحقيقي”، لأن الموجة تحتوي على قيم حقيقية، وليس معقدة. لاحقًا، سنرى تحويل فورييه الكامل، الذي يمكنه التعامل مع الإشارات المعقدة. نتيجة rfft، التي أسميها hs، هي مصفوفة NumPy من الأعداد المعقدة التي تمثل السعة والانزياح الطوري لكل مكون ترددي في الموجة.

نتيجة rfftfreq، التي أسميها fs، هي مصفوفة تحتوي على الترددات المقابلة لـ hs.

لفهم القيم في hs، اعتبر هذين الطريقتين للتفكير في الأعداد المعقدة:

  • العدد المعقد هو مجموع جزء حقيقي وجزء تخيلي، وغالبًا ما يُكتب ، حيث هو الوحدة التخيلية، . يمكنك التفكير في و كإحداثيات كارتيسية.

  • العدد المعقد هو أيضًا حاصل ضرب مقدار وعدد تخيلي أسّي، ، حيث هو المقدار و هو الزاوية بالراديان، وتسمى أيضًا “الحجة”. يمكنك التفكير في و كإحداثيات قطبية.

كل قيمة في hs تتوافق مع مكون ترددي: مقدارها يتناسب مع سعة المكون المقابل؛ وزاويتها هي الانزياح الطوري.

توفر فئة Spectrum خاصيتين للقراءة فقط، amps و angles، اللتين تعيدان مصفوفات NumPy تمثل المقدارات والزوايا لـ hs. عندما نقوم برسم كائن Spectrum، عادةً ما نرسم amps مقابل fs. أحيانًا يكون من المفيد أيضًا رسم angles مقابل fs.

على الرغم من أنه قد يكون مغريًا النظر إلى الأجزاء الحقيقية والتخيلية لـ hs، إلا أنك لن تحتاج تقريبًا إلى ذلك. أشجعك على التفكير في تحويل فورييه السريع (DFT) كمتجه من السعات والانحرافات الطورية التي تم تشفيرها في شكل أعداد مركبة.

لتعديل الطيف، يمكنك الوصول إلى hs مباشرة. على سبيل المثال:

spectrum.hs *= 2
spectrum.hs[spectrum.fs > cutoff] = 0

السطر الأول يضاعف عناصر hs بمقدار 2، مما يضاعف السعات لجميع المكونات. السطر الثاني يضبط إلى 0 فقط العناصر من hs حيث تتجاوز الترددات المقابلة بعض تردد القطع.

لكن الطيف يوفر أيضًا طرقًا لأداء هذه العمليات:

    spectrum.scale(2)
    spectrum.low_pass(cutoff)

يمكنك قراءة الوثائق الخاصة بهذه الطرق وغيرها على http://greenteapress.com/thinkdsp.html.

في هذه المرحلة، يجب أن يكون لديك فكرة أفضل عن كيفية عمل فئات الإشارة، والموجة، والطيف، لكنني لم أشرح كيفية عمل تحويل فورييه السريع بعد. سيستغرق ذلك بضع فصول أخرى.

التمارين

توجد حلول هذه التمارين في chap02soln.ipynb.

::: exercise التمرين 3.1. إذا كنت تستخدم Jupyter، قم بتحميل chap02.ipynb وجرب الأمثلة. يمكنك أيضًا عرض الدفتر على http://tinyurl.com/thinkdsp02. :::

::: exercise التمرين 3.2. لإشارة المنشار، يوجد شكل موجي يرتفع بشكل خطي من -1 إلى 1، ثم ينخفض إلى -1 ويتكرر. انظر http://en.wikipedia.org/wiki/Sawtooth_wave :::

اكتب فئة تسمى SawtoothSignal التي تمتد من Signal وتوفر evaluate لتقييم إشارة المنشار.

احسب طيف موجة المنشار. كيف يقارن الهيكل التوافقي بموجات مثلثية ومربعة؟ :::

::: exercise تمرين 3.3. اصنع إشارة مربعة بتردد 1100 هرتز واصنع موجة تأخذ عينات منها بمعدل 10000 إطار في الثانية. إذا قمت برسم الطيف، يمكنك أن ترى أن معظم التوافقيات قد تم تقليصها. عندما تستمع إلى الموجة، هل يمكنك سماع التوافقيات التي تم تقليصها؟ :::

::: exercise تمرين 3.4. إذا كان لديك كائن طيف، spectrum، وطبعت أولى القيم من spectrum.fs، سترى أنها تبدأ من الصفر. لذا فإن spectrum.hs[0] هو مقدار المكون الذي له تردد 0. لكن ماذا يعني ذلك؟

جرب هذه التجربة:

  1. اصنع إشارة مثلثية بتردد 440 واصنع موجة بمدة 0.01 ثانية. ارسم شكل الموجة.

  2. اصنع كائن طيف واطبع spectrum.hs[0]. ما هو السعة والطور لهذا المكون؟

  3. اضبط spectrum.hs[0] = 100. اصنع موجة من الطيف المعدل وارسمها. ما تأثير هذه العملية على شكل الموجة؟ :::

::: exercise تمرين 3.5. اكتب دالة تأخذ طيفًا كمعامل وتعدله عن طريق قسمة كل عنصر من hs على التردد المقابل من fs. تلميح: نظرًا لأن القسمة على الصفر غير معرفة، قد ترغب في ضبط spectrum.hs[0] = 0.

اختبر دالتك باستخدام موجة مربعة أو مثلثية أو موجة منشارية.

  1. احسب الطيف وارسمه.

  2. عدل الطيف باستخدام دالتك وارسمه مرة أخرى.

  3. اصنع موجة من الطيف المعدل واستمع إليها. ما تأثير هذه العملية على الإشارة؟ :::

::: exercise تمرين 3.6. تمتلك الموجات المثلثية والمربعة توافقيات فردية فقط؛ بينما تمتلك الموجة المنشارية توافقيات زوجية وفردية. تتناقص توافقيات الموجات المربعة والمنشارية بنسبة ؛ بينما تتناقص توافقيات الموجة المثلثية مثل . هل يمكنك إيجاد شكل موجي يمتلك توافقيات زوجية وفردية تتناقص مثل ؟

تلميح: هناك طريقتان يمكنك اتباعهما: يمكنك بناء الإشارة التي تريدها عن طريق جمع الموجات الجيبية، أو يمكنك البدء بإشارة مشابهة لما تريده وتعديلها. :::

الإشارات غير الدورية {#nonperiodic}

الإشارات التي عملنا معها حتى الآن هي دورية، مما يعني أنها تتكرر إلى الأبد. كما يعني ذلك أن مكونات التردد التي تحتويها لا تتغير مع مرور الوقت. في هذا الفصل، نعتبر الإشارات غير الدورية، التي تتغير مكونات ترددها مع مرور الوقت. بعبارة أخرى، تقريبًا جميع الإشارات الصوتية.

يقدم هذا الفصل أيضًا طيفيات، وهي طريقة شائعة لتصور الإشارات غير الدورية.

الكود الخاص بهذا الفصل موجود في chap03.ipynb، والذي يوجد في مستودع هذا الكتاب (انظر القسم 1.2{reference-type=“ref” reference=“code”}). يمكنك أيضًا مشاهدته على http://tinyurl.com/thinkdsp03.

الزقزوق الخطي

موجة الزقزوق بالقرب من البداية، الوسط، والنهاية.{#fig.chirp3 height=“2.5in”}

سنبدأ بـ الزقزوق، وهو إشارة ذات تردد متغير. يوفر thinkdsp إشارة تُسمى Chirp تُنتج جيبًا يهتز بشكل خطي عبر مجموعة من الترددات.

إليك مثالاً يمتد من 220 إلى 880 هرتز، وهو ما يعادل أوكتافين من A3 إلى A5:

signal = thinkdsp.Chirp(start=220, end=880)
wave = signal.make_wave()

تظهر الشكل 4.1{reference-type=“ref” reference=“fig.chirp3} أجزاء من هذه الموجة بالقرب من البداية، الوسط، والنهاية. من الواضح أن التردد في تزايد.

قبل أن نتابع، دعونا نرى كيف يتم تنفيذ Chirp. إليك تعريف الفئة:

class Chirp(Signal):

    def __init__(self, start=440, end=880, amp=1.0):
        self.start = start
        self.end = end
        self.amp = amp

start و end هما الترددات، بوحدة الهرتز، في بداية ونهاية الزقزوق. amp هو السعة.

إليك الوظيفة التي تقيم الإشارة:

    def evaluate(self, ts):
        freqs = np.linspace(self.start, self.end, len(ts))
        dts = np.diff(ts, prepend=0)
        dphis = PI2 * freqs * dts
        phases = np.cumsum(dphis)
        ys = self.amp * np.cos(phases)
        return ys

ts هو تسلسل النقاط الزمنية التي ينبغي تقييم الإشارة عندها؛ للحفاظ على هذه الوظيفة بسيطة، أفترض أنها متباعدة بالتساوي.

لحساب التردد عند كل نقطة زمنية، أستخدم np.linspace، والذي يعيد مصفوفة NumPy من قيم بين start و end.

تحسب np.diff الفرق بين العناصر المجاورة لـ ts، عائدةً بطول كل فترة زمنية بالثواني. إذا كانت عناصر ts متباعدة بالتساوي، فإن dts ستكون جميعها متساوية.

الخطوة التالية هي معرفة مقدار تغير الطور خلال كل فترة. في القسم 2.7{reference-type=“ref” reference=“sigobs”} رأينا أنه عندما يكون التردد ثابتًا، فإن الطور، ، يزداد خطيًا مع مرور الوقت: عندما يكون التردد دالة للزمن، فإن التغير في الطور خلال فترة زمنية قصيرة، هو: في بايثون، بما أن freqs تحتوي على و dts تحتوي على فترات الزمن، يمكننا كتابة

dphis = PI2 * freqs * dts

الآن، بما أن dphis تحتوي على التغيرات في الطور، يمكننا الحصول على الطور الكلي عند كل خطوة زمنية من خلال جمع التغيرات:

phases = np.cumsum(dphis)
phases = np.insert(phases, 0, 0)

تحسب np.cumsum المجموع التراكمي، وهو تقريبًا ما نريده، ولكنه لا يبدأ من 0. لذلك أستخدم np.insert لإضافة 0 في البداية.

النتيجة هي مصفوفة NumPy حيث يحتوي العنصر i على مجموع أول i حدود من dphis؛ أي، الطور الكلي في نهاية الفاصل الزمني i. أخيرًا، تقوم np.cos بحساب سعة الموجة كدالة للطور (تذكر أن الطور يُعبر عنه بالراديان).

إذا كنت تعرف حساب التفاضل والتكامل، قد تلاحظ أن الحد عندما تصبح صغيرة هو قسمة كلا الجانبين على ينتج بعبارة أخرى، التردد هو مشتق الطور. وعلى العكس، الطور هو تكامل التردد. عندما استخدمنا cumsum للانتقال من التردد إلى الطور، كنا نقوم بتقريب التكامل.

النغمة الأسية

عندما تستمع إلى هذه النغمة، قد تلاحظ أن درجة الصوت ترتفع بسرعة في البداية ثم تتباطأ. تمتد النغمة عبر اثنين من الأوكتافات، لكنها تستغرق فقط 2/3 ثانية لتغطية الأوكتاف الأول، ومرتين من الوقت لتغطية الأوكتاف الثاني.

السبب هو أن إدراكنا لدرجة الصوت يعتمد على لوغاريتم التردد. نتيجة لذلك، فإن الفاصل الذي نسمعه بين نغمتيْن يعتمد على نسبة تردديهما، وليس الفرق. “الفاصل” هو المصطلح الموسيقي للاختلاف المدرك بين درجتي صوت.

على سبيل المثال، الأوكتاف هو فاصل حيث تكون نسبة درجتي صوت هي 2. لذا فإن الفاصل من 220 إلى 440 هو أوكتاف واحد والفاصل من 440 إلى 880 هو أيضًا أوكتاف واحد. الفرق في التردد أكبر، لكن النسبة هي نفسها.

نتيجة لذلك، إذا زادت الترددات بشكل خطي، كما في تذبذب خطي، فإن درجة الصوت المدركة تزداد بشكل لوغاريتمي.

إذا كنت ترغب في زيادة درجة الصوت المدركة بشكل خطي، يجب أن تزداد الترددات بشكل أسي. يُطلق على الإشارة التي تحمل هذا الشكل اسم تذبذب أسي.

إليك الشيفرة التي تصنع واحدة:

class ExpoChirp(Chirp):

    def evaluate(self, ts):
        start, end = np.log10(self.start), np.log10(self.end)
        freqs = np.logspace(start, end, len(ts)-1)
        return self._evaluate(ts, freqs)

بدلاً من np.linspace، تستخدم هذه النسخة من evaluate np.logspace، التي تنشئ سلسلة من الترددات التي تكون لوغاريتماتها متباعدة بشكل متساوٍ، مما يعني أنها تزداد بشكل أسي.

هذا كل شيء؛ كل شيء آخر هو نفسه كما في تذبذب Chirp. إليك الشيفرة التي تصنع واحدة:

    signal = thinkdsp.ExpoChirp(start=220, end=880)
    wave = signal.make_wave(duration=1)

يمكنك الاستماع إلى هذه الأمثلة في chap03.ipynb ومقارنة التذبذبات الخطية والأسيّة.

طيف التذبذب {#sauron}

طيف تذبذب واحد أوكتاف لمدة ثانية واحدة.{#fig.chirp1 height=“2.5in”}

ماذا تعتقد يحدث إذا قمت بحساب طيف التذبذب؟ إليك مثالاً يبني تذبذباً واحداً أوكتاف لمدة ثانية واحدة وطيفه:

    signal = thinkdsp.Chirp(start=220, end=440)
    wave = signal.make_wave(duration=1)
    spectrum = wave.make_spectrum()

الشكل 4.2{reference-type=“ref” reference=“fig.chirp1”} يُظهر النتيجة. الطيف يحتوي على مكونات عند كل تردد من 220 إلى 440 هرتز، مع تباينات تبدو قليلاً مثل عين سورون (انظر http://en.wikipedia.org/wiki/Sauron).

الطيف تقريبًا مستوٍ بين 220 و440 هرتز، مما يشير إلى أن الإشارة تقضي وقتًا متساويًا عند كل تردد في هذه النطاق. استنادًا إلى هذه الملاحظة، يجب أن تكون قادرًا على تخمين كيف يبدو طيف الرفرفة الأسية.

يوفر الطيف تلميحات حول بنية الإشارة، ولكنه يُخفي العلاقة بين التردد والزمن. على سبيل المثال، لا يمكننا أن نعرف من خلال النظر إلى هذا الطيف ما إذا كان التردد قد ارتفع أو انخفض، أو كلاهما.

الطيف الزمني

طيف زمني لرفرفة واحدة لمدة ثانية واحدة.{#fig.chirp2 height=“2.5in”}

لاستعادة العلاقة بين التردد والزمن، يمكننا تقسيم الرفرفة إلى مقاطع ورسم طيف كل مقطع. النتيجة تُسمى تحويل فورييه قصير الزمن (STFT).

هناك عدة طرق لتصور STFT، ولكن الأكثر شيوعًا هو الطيف الزمني، الذي يُظهر الزمن على المحور السيني والتردد على المحور الصادي. كل عمود في الطيف الزمني يُظهر طيف مقطع قصير، باستخدام اللون أو التدرج الرمادي لتمثيل السعة.

كمثال، سأحسب الطيف الزمني لهذه الرفرفة:

signal = thinkdsp.Chirp(start=220, end=440)
wave = signal.make_wave(duration=1, framerate=11025)

تقدم Wave دالة make_spectrogram، التي تُرجع كائن Spectrogram:

spectrogram = wave.make_spectrogram(seg_length=512)
spectrogram.plot(high=700)

seg_length هو عدد العينات في كل جزء. اخترت 512 لأن FFT تكون أكثر كفاءة عندما يكون عدد العينات قوة من 2.

تظهر الشكل 4.3{reference-type=“ref” reference=“fig.chirp2”} النتيجة. المحور السيني يُظهر الزمن من 0 إلى 1 ثانية. المحور الصادي يُظهر التردد من 0 إلى 700 هرتز. لقد قمت بقص الجزء العلوي من الطيف؛ النطاق الكامل يصل إلى 5512.5 هرتز، وهو نصف معدل الإطارات.

يظهر الطيف بوضوح أن التردد يزيد بشكل خطي مع مرور الوقت. بالمثل، في طيف النبضة الأسية، يمكننا رؤية شكل المنحنى الأسّي.

ومع ذلك، لاحظ أن القمة في كل عمود مشوشة عبر 2—3 خلايا. يعكس هذا التشويش الدقة المحدودة للطيف.

حد غابور {#gabor}

إن دقة الزمن للطيف هي مدة الأجزاء، التي تتوافق مع عرض الخلايا في الطيف. بما أن كل جزء يتكون من 512 إطارًا، وهناك 11,025 إطارًا في الثانية، فإن مدة كل جزء تبلغ حوالي 0.046 ثانية.

أما دقة التردد فهي نطاق التردد بين العناصر في الطيف، والتي تتوافق مع ارتفاع الخلايا. مع 512 إطارًا، نحصل على 256 مكونًا تردديًا على نطاق من 0 إلى 5512.5 هرتز، لذا فإن النطاق بين المكونات هو 21.6 هرتز.

بشكل عام، إذا كانت هي طول المقطع، فإن الطيف يحتوي على مكون. إذا كانت معدل الإطارات ، فإن التردد الأقصى في الطيف هو . لذا فإن دقة الزمن هي ودقة التردد هي والتي تعادل .

نود في المثالي أن تكون دقة الزمن صغيرة، حتى نتمكن من رؤية التغيرات السريعة في التردد. ونود أن تكون دقة التردد صغيرة حتى نتمكن من رؤية التغيرات الصغيرة في التردد. ولكن لا يمكنك الحصول على كلاهما. لاحظ أن دقة الزمن، ، هي معكوس دقة التردد، . لذا إذا أصبحت واحدة أصغر، فإن الأخرى ستصبح أكبر.

على سبيل المثال، إذا قمت بمضاعفة طول المقطع، فإنك تقلل دقة التردد إلى النصف (وهو أمر جيد)، لكنك تضاعف دقة الزمن (وهو أمر سيء). حتى زيادة معدل الإطارات لا تساعد. ستحصل على المزيد من العينات، لكن نطاق الترددات يزداد في نفس الوقت.

تسمى هذه المقايضة حد غابور وهي قيد أساسي من قيود هذا النوع من تحليل الزمن-التردد.

التسرب

طيف مقطع دوري من جيب التمام (يسار)، مقطع غير دوري (وسط)، مقطع غير دوري مع نافذة (يمين).{#fig.windowing1 width=“5.5in”}

لكي أشرح كيف تعمل make_spectrogram، يجب أن أشرح مفهوم النافذة؛ ومن أجل شرح مفهوم النافذة، يجب أن أظهر لك المشكلة التي تهدف إلى معالجتها، وهي التسرب.

تحويل فورييه المتقطع (DFT)، الذي نستخدمه لحساب الطيف، يعامل الموجات كما لو كانت دورية؛ أي أنه يفترض أن الجزء المحدود الذي يعمل عليه هو فترة كاملة من إشارة لا نهائية تتكرر عبر الزمن. في الممارسة العملية، يكون هذا الافتراض غالبًا غير صحيح، مما يخلق مشاكل.

تتمثل إحدى المشاكل الشائعة في الانقطاع في بداية ونهاية الجزء. نظرًا لأن DFT يفترض أن الإشارة دورية، فإنه يربط بشكل ضمني نهاية الجزء ببداية الجزء لتكوين حلقة. إذا لم تتصل النهاية بسلاسة بالبداية، فإن الانقطاع يخلق مكونات تردد إضافية في الجزء غير موجودة في الإشارة.

كمثال، دعونا نبدأ بإشارة جيبية تحتوي على مكون تردد واحد فقط عند 440 هرتز.

    signal = thinkdsp.SinSignal(freq=440)

إذا اخترنا جزءًا يصادف أن يكون مضاعفًا صحيحًا للفترة، فإن نهاية الجزء تتصل بسلاسة بالبداية، ويتصرف DFT بشكل جيد.

    duration = signal.period * 30
    wave = signal.make_wave(duration)
    spectrum = wave.make_spectrum()

تظهر الشكل 4.4{reference-type=“ref” reference=“fig.windowing1”} (يسار) النتيجة. كما هو متوقع، هناك قمة واحدة عند 440 هرتز.

ولكن إذا لم تكن المدة مضاعفًا للفترة، تحدث أشياء سيئة. مع duration = signal.period * 30.25، تبدأ الإشارة عند 0 وتنتهي عند 1.

الشكل 4.4{reference-type=“ref” reference=“fig.windowing1”} (الوسط) يُظهر طيف هذه القطعة. مرة أخرى، الذروة عند 440 هرتز، ولكن الآن هناك مكونات إضافية تمتد من 240 إلى 640 هرتز. تُسمى هذه الانتشار تسرب الطيف، لأن بعض الطاقة التي تكون فعليًا عند التردد الأساسي تتسرب إلى ترددات أخرى.

في هذا المثال، يحدث التسرب لأننا نستخدم DFT على قطعة تصبح غير متصلة عندما نتعامل معها كدورية.

التقطيع

قطعة من دالة جيب (الأعلى)، نافذة هامينغ (الوسط)، ناتج القطعة والنافذة (الأسفل).{#fig.windowing2 height=“3.5in”}

يمكننا تقليل التسرب من خلال تلطيف الانقطاع بين بداية ونهاية القطعة، وأحد الطرق للقيام بذلك هو التقطيع.

“النافذة” هي دالة مصممة لتحويل قطعة غير دورية إلى شيء يمكن أن يُعتبر دوريًا. الشكل 4.5{reference-type=“ref” reference=“fig.windowing2”} (الأعلى) يُظهر قطعة حيث لا تتصل النهاية بسلاسة مع البداية.

الشكل 4.5{reference-type=“ref” reference=“fig.windowing2”} (الوسط) يُظهر “نافذة هامينغ”، واحدة من أكثر دوال النوافذ شيوعًا. لا توجد دالة نافذة مثالية، ولكن يمكن إظهار أن بعضها مثالي لتطبيقات مختلفة، وتعتبر هامينغ نافذة جيدة لجميع الأغراض.

الشكل 4.5{reference-type=“ref” reference=“fig.windowing2”} (أسفل) يُظهر نتيجة ضرب النافذة في الإشارة الأصلية. حيث تكون النافذة قريبة من 1، تبقى الإشارة دون تغيير. وحيث تكون النافذة قريبة من 0، تتعرض الإشارة للتخفيف. نظرًا لأن النافذة تتناقص عند كلا الطرفين، يتصل نهاية الجزء بسلاسة مع البداية.

الشكل 4.4{reference-type=“ref” reference=“fig.windowing1”} (يمين) يُظهر طيف الإشارة المقطعة. لقد خفّضت عملية استخدام النافذة التسرب بشكل كبير، ولكن ليس تمامًا.

إليك كيف يبدو الكود. Wave توفر window، التي تطبق نافذة هامينغ:

#class Wave:
    def window(self, window):
        self.ys *= window

ويوفر NumPy hamming، الذي يحسب نافذة هامينغ بطول معين:

window = np.hamming(len(wave))
wave.window(window)

يوفر NumPy دوالًا لحساب دوال نوافذ أخرى، بما في ذلك bartlett، blackman، hanning، و kaiser. واحدة من التمارين في نهاية هذا الفصل تطلب منك التجربة مع هذه النوافذ الأخرى.

تنفيذ الطيف الزمني

نوافذ هامينغ المتداخلة.{#fig.windowing3 height=“2.5in”}

الآن بعد أن فهمنا مفهوم النوافذ، يمكننا فهم تنفيذ الطيف الزمني. إليك طريقة Wave التي تحسب الأطياف الزمنية:

#class Wave:
    def make_spectrogram(self, seg_length):
        window = np.hamming(seg_length)
        i, j = 0, seg_length
        step = seg_length / 2

        spec_map = {}

        while j < len(self.ys):
            segment = self.slice(i, j)
            segment.window(window)

            t = (segment.start + segment.end) / 2
            spec_map[t] = segment.make_spectrum()

            i += step
            j += step

        return Spectrogram(spec_map, seg_length)

هذه هي أطول دالة في الكتاب، لذا إذا كنت تستطيع التعامل مع ذلك، يمكنك التعامل مع أي شيء.

المعلمة، self، هي كائن من نوع Wave. seg_length هو عدد العينات في كل قطعة.

window هو نافذة هامينغ بنفس طول القطع.

i و j هما مؤشرات الشريحة التي تحدد القطع من الموجة. step هو الإزاحة بين القطع. نظرًا لأن step هو نصف seg_length، فإن القطع تتداخل بنصف.

تظهر الشكل 4.6{reference-type=“ref” reference=“fig.windowing3”} كيف تبدو هذه النوافذ المتداخلة.

spec_map هو قاموس يربط من طابع زمني إلى طيف.

داخل حلقة while، نختار شريحة من الموجة ونطبق النافذة؛ ثم نبني كائن Spectrum ونضيفه إلى spec_map. الوقت الاسمي لكل قطعة، t، هو منتصف الطريق.

ثم نتقدم بـ i و j، ونستمر طالما أن j لا يتجاوز نهاية الموجة.

أخيرًا، تقوم الدالة بإنشاء وإرجاع طيف زمني. إليك تعريف الطيف الزمني:

class Spectrogram(object):
    def __init__(self, spec_map, seg_length):
        self.spec_map = spec_map
        self.seg_length = seg_length

مثل العديد من طرق init، هذه الطريقة تخزن المعلمات كسمات.

يوفر Spectrogram دالة plot، التي تولد رسمًا بالألوان الزائفة مع الوقت على المحور السيني والتردد على المحور الصادي.

وهكذا يتم تنفيذ الأطياف الزمنية.

التمارين

توجد حلول هذه التمارين في chap03soln.ipynb.

::: exercise التمرين 4.1. قم بتشغيل والاستماع إلى الأمثلة في chap03.ipynb، الموجودة في مستودع هذا الكتاب، والمتاحة أيضًا على http://tinyurl.com/thinkdsp03.

في مثال التسرب، حاول استبدال نافذة هامينغ بإحدى النوافذ الأخرى المقدمة من NumPy، وانظر ما التأثير الذي تتركه على التسرب. انظر http://docs.scipy.org/doc/numpy/reference/routines.window.html :::

::: exercise التمرين 4.2. اكتب فئة تسمى SawtoothChirp التي تمتد من Chirp وتعيد تعريف evaluate لتوليد شكل موجي متعرج بتردد يتزايد (أو يتناقص) بشكل خطي.

تلميح: اجمع بين دوال التقييم من Chirp و SawtoothSignal.

ارسم تخطيطًا لما تعتقد أن طيف هذه الإشارة سيبدو عليه، ثم قم برسمه. يجب أن يكون تأثير التداخل مرئيًا، وإذا استمعت بعناية، يمكنك سماعه. :::

::: exercise التمرين 4.3. اصنع صرخة متعرجة تتراوح من 2500 إلى 3000 هرتز، ثم استخدمها لصنع موجة مدتها 1 ثانية ومعدل إطارات 20 كيلوهرتز. ارسم تخطيطًا لما تعتقد أن الطيف سيبدو عليه. ثم قم برسم الطيف وانظر إذا كنت قد حصلت عليه بشكل صحيح. :::

::: exercise التمرين 4.4. في المصطلحات الموسيقية، “الزلق” هو نوتة تنزلق من نغمة إلى أخرى، لذا فهو مشابه للصرخة.

ابحث عن تسجيل لغليساندو أو قم بإنشائه ورسم طيف صوتي لعدة ثوانٍ الأولى. اقتراح واحد: تبدأ رابسودي في الأزرق لجورج غيرشوين بغليساندو كلارينيت مشهور، والذي يمكنك تنزيله من http://archive.org/details/rhapblue11924.

:::

::: تمرين تمرين 4.5. يمكن لعازف التروبيم أن يعزف غليساندو من خلال تمديد شريحة التروبيم أثناء النفخ باستمرار. مع تمدد الشريحة، يزداد الطول الكلي للأنبوب، ويكون النغمة الناتجة عكسية بالنسبة للطول.

افترض أن اللاعب يحرك الشريحة بسرعة ثابتة، كيف تتغير الترددات مع الزمن؟

اكتب فئة تسمى TromboneGliss تمتد من Chirp وتوفر evaluate. اصنع موجة تحاكي غليساندو التروبيم من C3 إلى F3 ثم العودة إلى C3. C3 هو 262 هرتز؛ F3 هو 349 هرتز.

ارسم طيفًا صوتيًا للموجة الناتجة. هل غليساندو التروبيم يشبه أكثر التشيرب الخطي أم الأسي؟ :::

::: تمرين تمرين 4.6. قم بإنشاء أو العثور على تسجيل لسلسلة من أصوات الحروف المتحركة وانظر إلى الطيف الصوتي. هل يمكنك تحديد حروف متحركة مختلفة؟ :::

الضجيج

في اللغة الإنجليزية، تعني “الضجيج” صوتًا غير مرغوب فيه أو غير مريح. في سياق معالجة الإشارات، لها معنيان مختلفان:

  1. مثلما في الإنجليزية، يمكن أن تعني إشارة غير مرغوب فيها من أي نوع. إذا تداخلت إشارتان مع بعضهما البعض، فإن كل إشارة ستعتبر الأخرى ضجيجًا.

  2. “الضجيج” يشير أيضًا إلى إشارة تحتوي على مكونات عند العديد من الترددات، مما يعني أنها تفتقر إلى الهيكل التوافقي للإشارات الدورية التي رأيناها في الفصول السابقة.

هذا الفصل يتعلق بالنوع الثاني.

الكود الخاص بهذا الفصل موجود في chap04.ipynb، والذي يوجد في مستودع هذا الكتاب (انظر القسم 1.2{reference-type=“ref” reference=“code”}). يمكنك أيضًا مشاهدته على http://tinyurl.com/thinkdsp04.

الضجيج غير المرتبط

شكل موجة الضجيج غير المرتبط المتجانس.{#fig.whitenoise0 height=“2.5in”}

أبسط طريقة لفهم الضجيج هي توليده، وأبسط نوع لتوليده هو الضجيج المتجانس غير المرتبط (UU noise). “متجانس” يعني أن الإشارة تحتوي على قيم عشوائية من توزيع متجانس؛ أي أن كل قيمة في النطاق لها نفس الاحتمالية. “غير مرتبط” يعني أن القيم مستقلة؛ أي أن معرفة قيمة واحدة لا توفر أي معلومات عن القيم الأخرى.

إليك فئة تمثل الضجيج غير المرتبط المتجانس:

class UncorrelatedUniformNoise(_Noise):

    def evaluate(self, ts):
        ys = np.random.uniform(-self.amp, self.amp, len(ts))
        return ys

UncorrelatedUniformNoise ترث من _Noise، التي ترث بدورها من Signal.

كما هو معتاد، تأخذ دالة التقييم ts، الأوقات التي يجب تقييم الإشارة فيها. تستخدم np.random.uniform، التي تولد القيم من توزيع متجانس. في هذا المثال، تكون القيم في النطاق بين -amp إلى amp.

المثال التالي يولد ضوضاء UU تستمر لمدة 0.5 ثانية بمعدل 11,025 عينة في الثانية.

signal = thinkdsp.UncorrelatedUniformNoise()
wave = signal.make_wave(duration=0.5, framerate=11025)

إذا قمت بتشغيل هذه الموجة، فإنها تبدو مثل الضوضاء التي تسمعها إذا قمت بضبط راديو بين القنوات. الشكل 5.1{reference-type=“ref” reference=“fig.whitenoise0”} يظهر كيف تبدو الموجة. كما هو متوقع، تبدو عشوائية إلى حد كبير.

طيف الطاقة للضوضاء الموحدة غير المرتبطة.{#fig.whitenoise1 height=“2.5in”}

الآن دعنا نلقي نظرة على الطيف:

spectrum = wave.make_spectrum()
spectrum.plot_power()

Spectrum.plot_power مشابه لـ Spectrum.plot، باستثناء أنه يرسم الطاقة بدلاً من السعة. الطاقة هي مربع السعة. أنا أتحول من السعة إلى الطاقة في هذا الفصل لأنه أكثر تقليدية في سياق الضوضاء.

الشكل 5.2{reference-type=“ref” reference=“fig.whitenoise1”} يظهر النتيجة. مثل الإشارة، يبدو الطيف عشوائيًا إلى حد كبير. في الواقع، هو عشوائي، ولكن يجب أن نكون أكثر دقة بشأن كلمة “عشوائي”. هناك على الأقل ثلاث أشياء قد نود معرفتها عن إشارة الضوضاء أو طيفها:

  • التوزيع: إن توزيع إشارة عشوائية هو مجموعة القيم الممكنة واحتمالاتها. على سبيل المثال، في إشارة الضوضاء المتجانسة، مجموعة القيم هي النطاق من -1 إلى 1، وجميع القيم لها نفس الاحتمالية. بديل لذلك هو ضوضاء غاوسية، حيث مجموعة القيم هي النطاق من السالب إلى الموجب اللانهاية، ولكن القيم القريبة من 0 هي الأكثر احتمالًا، مع احتمال يتناقص وفقًا لمنحنى غاوسي أو “جرس”.

  • الارتباط: هل كل قيمة في الإشارة مستقلة عن الأخرى، أم توجد تبعيات بينها؟ في ضوضاء UU، القيم مستقلة. بديل لذلك هو الضوضاء البراونية، حيث كل قيمة هي مجموع القيمة السابقة و”خطوة” عشوائية. لذا إذا كانت قيمة الإشارة مرتفعة في نقطة زمنية معينة، نتوقع أن تبقى مرتفعة، وإذا كانت منخفضة، نتوقع أن تبقى منخفضة.

  • العلاقة بين القدرة والتردد: في طيف ضوضاء UU، القدرة عند جميع الترددات مأخوذة من نفس التوزيع؛ أي أن القدرة المتوسطة هي نفسها لجميع الترددات. بديل لذلك هو الضوضاء الوردية، حيث ترتبط القدرة عكسيًا بالتردد؛ أي أن القدرة عند التردد مأخوذة من توزيع متوسطه متناسب مع .

الطيف المتكامل

بالنسبة لضجيج UU، يمكننا رؤية العلاقة بين الطاقة والتردد بشكل أوضح من خلال النظر إلى الطيف المتكامل، وهو دالة للتردد، ، تُظهر الطاقة التراكمية في الطيف حتى .

الطيف المتكامل للضجيج الموحد غير المرتبط.{#fig.whitenoise2 height=“2.5in”}

يوفر Spectrum طريقة تحسب الطيف المتكامل:

def make_integrated_spectrum(self):
    cs = np.cumsum(self.power)
    cs /= cs[-1]
    return IntegratedSpectrum(cs, self.fs)

self.power هو مصفوفة NumPy تحتوي على الطاقة لكل تردد. تقوم np.cumsum بحساب المجموع التراكمي للطاقة. تقسيم المصفوفة على العنصر الأخير يطبع الطيف المتكامل بحيث يتراوح من 0 إلى 1.

النتيجة هي IntegratedSpectrum. هنا تعريف الفئة:

class IntegratedSpectrum(object):
    def __init__(self, cs, fs):
        self.cs = cs
        self.fs = fs

مثل Spectrum، يوفر IntegratedSpectrum دالة plot_power، لذا يمكننا حساب ورسم الطيف المتكامل بهذه الطريقة:

integ = spectrum.make_integrated_spectrum()
integ.plot_power()

النتيجة، الموضحة في الشكل 5.3{reference-type=“ref” reference=“fig.whitenoise2”}، هي خط مستقيم، مما يدل على أن الطاقة عند جميع الترددات ثابتة، في المتوسط. يُطلق على الضوضاء التي تمتلك طاقة متساوية عند جميع الترددات اسم الضوضاء البيضاء، بالتشبيه مع الضوء، لأن المزيج المتساوي من الضوء عند جميع الترددات المرئية هو أبيض.

الضوضاء البراونية {#brownian}

موجة الضوضاء البراونية.{#fig.rednoise0 height=“2.5in”}

الضوضاء UU غير مترابطة، مما يعني أن كل قيمة لا تعتمد على القيم الأخرى. بديل آخر هو الضوضاء البراونية، حيث تكون كل قيمة هي مجموع القيمة السابقة و”خطوة” عشوائية.

تُسمى “براونية” بالتشبيه مع الحركة البراونية، حيث تتحرك جزيئة معلقة في سائل بشكل يبدو عشوائيًا، بسبب تفاعلات غير مرئية مع السائل. غالبًا ما تُوصف الحركة البراونية باستخدام المشي العشوائي، وهو نموذج رياضي لمسار حيث يتميز المسافة بين الخطوات بتوزيع عشوائي.

في مشي عشوائي أحادي البعد، تتحرك الجزيئة لأعلى أو لأسفل بمقدار عشوائي في كل خطوة زمنية. موقع الجزيئة في أي نقطة زمنية هو مجموع جميع الخطوات السابقة.

تشير هذه الملاحظة إلى طريقة لتوليد الضوضاء البراونية: توليد خطوات عشوائية غير مترابطة ثم جمعها. إليك تعريف فئة ينفذ هذا الخوارزم:

class BrownianNoise(_Noise):

    def evaluate(self, ts):
        dys = np.random.uniform(-1, 1, len(ts))
        ys = np.cumsum(dys)
        ys = normalize(unbias(ys), self.amp)
        return ys

evaluate يستخدم np.random.uniform لتوليد إشارة غير مرتبطة و np.cumsum لحساب مجموعها التراكمي.

نظرًا لأن المجموع من المحتمل أن يتجاوز النطاق من -1 إلى 1، يجب علينا استخدام unbias لنقل المتوسط إلى 0، و normalize للحصول على السعة القصوى المطلوبة.

إليك الكود الذي يولد كائن BrownianNoise ويرسم شكل الموجة.

signal = thinkdsp.BrownianNoise()
wave = signal.make_wave(duration=0.5, framerate=11025)
wave.plot()

تظهر الشكل 5.4{reference-type=“ref” reference=“fig.rednoise0”} النتيجة. تتجول شكل الموجة لأعلى ولأسفل، لكن هناك ارتباط واضح بين القيم المتعاقبة. عندما تكون السعة مرتفعة، تميل إلى البقاء مرتفعة، والعكس صحيح.

طيف الضوضاء البراونية على مقياس خطي (يسار) ومقياس لوغاريتمي-لوغاريتمي (يمين).{#fig.rednoise3 height=“2.5in”}

إذا قمت برسم طيف الضوضاء البراونية على مقياس خطي، كما في الشكل 5.5{reference-type=“ref” reference=“fig.rednoise3”} (يسار)، فإنه لا يبدو كثيرًا. تقريبًا كل الطاقة توجد عند أدنى الترددات؛ المكونات ذات الترددات الأعلى غير مرئية.

لرؤية شكل الطيف بشكل أوضح، يمكننا رسم الطاقة والتردد على مقياس لوغاريتمي-لوغاريتمي. إليك الكود:

import matplotlib.pyplot as plt
 
spectrum = wave.make_spectrum()
spectrum.plot_power(linewidth=1, alpha=0.5)
plt.xscale('log')
plt.yscale('log')

النتيجة موضحة في الشكل 5.5{reference-type=“ref” reference=“fig.rednoise3”} (يمين). العلاقة بين القدرة والتردد متقلبة، لكنها تقريبًا خطية.

Spectrum يوفر estimate_slope، الذي يستخدم SciPy لحساب ملاءمة المربعات الصغرى لطيف القدرة:

#class Spectrum

    def estimate_slope(self):
        x = np.log(self.fs[1:])
        y = np.log(self.power[1:])
        t = scipy.stats.linregress(x,y)
        return t

يتجاهل المكون الأول من الطيف لأن هذا المكون يتوافق مع ، و غير معرف.

تُرجع estimate_slope النتيجة من scipy.stats.linregress، وهو كائن يحتوي على الميل المقدر والتقاطع، ومعامل التحديد ()، وقيمة p، والخطأ القياسي. لأغراضنا، نحتاج فقط إلى الميل.

بالنسبة للضوضاء البراونية، فإن ميل طيف القدرة هو -2 (سنرى لماذا في الفصل 10{reference-type=“ref” reference=“diffint”})، لذا يمكننا كتابة هذه العلاقة: حيث هو القدرة، و هو التردد، و هو تقاطع الخط، والذي ليس مهمًا لأغراضنا. إذا قمنا بتطبيق الأس على كلا الجانبين نحصل على: حيث هو ، لكنه لا يزال غير مهم. ما هو أكثر أهمية هو أن القدرة تتناسب مع ، وهو ما يميز الضوضاء البراونية.

الضوضاء البراونية تُعرف أيضًا باسم الضوضاء الحمراء، لنفس السبب الذي يُطلق به على الضوضاء البيضاء “بيضاء”. إذا قمت بدمج الضوء المرئي مع طاقة تتناسب مع ، فإن معظم الطاقة ستكون في الطرف المنخفض التردد من الطيف، والذي هو الأحمر. تُسمى الضوضاء البراونية أحيانًا “الضوضاء البنية”، لكنني أعتقد أن ذلك مُربك، لذا لن أستخدمه.

الضوضاء الوردية {#pink}

شكل موجة الضوضاء الوردية مع
\beta=1.{#fig.pinknoise0 height=“2.5in”}

بالنسبة للضوضاء الحمراء، العلاقة بين التردد والطاقة هي لا يوجد شيء مميز حول الأس 2. بشكل أكثر عمومية، يمكننا توليد الضوضاء بأي أس، . عندما ، تكون الطاقة ثابتة عند جميع الترددات، لذا فإن النتيجة هي الضوضاء البيضاء. عندما تكون النتيجة هي الضوضاء الحمراء.

عندما تكون بين 0 و 2، تكون النتيجة بين الضوضاء البيضاء والحمراء، لذا تُسمى الضوضاء الوردية.

هناك عدة طرق لتوليد الضوضاء الوردية. أبسطها هو توليد الضوضاء البيضاء ثم تطبيق فلتر منخفض التردد بالأس المطلوب. توفر thinkdsp فئة تمثل إشارة الضوضاء الوردية:

class PinkNoise(_Noise):

    def __init__(self, amp=1.0, beta=1.0):
        self.amp = amp
        self.beta = beta

amp هو السعة المطلوبة للإشارة. beta هو الأس المطلوب. توفر PinkNoise دالة make_wave، التي تولد موجة.

    def make_wave(self, duration=1, start=0, framerate=11025):
        signal = UncorrelatedUniformNoise()
        wave = signal.make_wave(duration, start, framerate)
        spectrum = wave.make_spectrum()

        spectrum.pink_filter(beta=self.beta)

        wave2 = spectrum.make_wave()
        wave2.unbias()
        wave2.normalize(self.amp)
        return wave2

المدة هي طول الموجة بالثواني. البداية هي وقت بدء الموجة؛ تم تضمينها لكي تكون make_wave لها نفس الواجهة لجميع أنواع الإشارات، ولكن بالنسبة للضوضاء العشوائية، فإن وقت البدء غير ذي صلة. ومعدل الإطارات هو عدد العينات في الثانية.

طيف الضوضاء البيضاء والوردية والحمراء على مقياس لوغاريتمي.{#fig.noise-triple height=“2.5in”}

تقوم make_wave بإنشاء موجة ضوضاء بيضاء، وتحسب طيفها، وتطبق فلترًا بالأس المطلوب، ثم تقوم بتحويل الطيف المفلتر مرة أخرى إلى موجة. بعد ذلك، تقوم بإزالة التحيز وتطبيع الموجة.

توفر Spectrum pink_filter:

    def pink_filter(self, beta=1.0):
        denom = self.fs ** (beta/2.0)
        denom[0] = 1
        self.hs /= denom

يقوم pink_filter بقسمة كل عنصر من الطيف على . نظرًا لأن الطاقة هي مربع السعة، فإن هذه العملية تقسم الطاقة في كل مكون على . وتعامل المكون عند كحالة خاصة، جزئيًا لتجنب القسمة على 0، وجزئيًا لأن هذا العنصر يمثل تحيز الإشارة، الذي سنقوم بضبطه على 0 على أي حال.

تظهر الشكل 5.6{reference-type=“ref” reference=“fig.pinknoise0”} الموجة الناتجة. مثل الضوضاء البراونية، تتجول لأعلى ولأسفل بطريقة توحي بوجود ارتباط بين القيم المتعاقبة، ولكن على الأقل بصريًا، تبدو أكثر عشوائية.في الفصل التالي، سنعود إلى هذه الملاحظة وسأكون أكثر دقة بشأن ما أعنيه بـ “الارتباط” و “الأكثر عشوائية”.

أخيرًا، تُظهر الشكل 5.7{reference-type=“ref” reference=“fig.noise-triple”} طيفًا للضوضاء البيضاء والوردية والحمراء على نفس المقياس اللوغاريتمي. العلاقة بين الأس، ، وانحدار الطيف واضحة في هذا الشكل.

الضوضاء الغاوسية

مخطط الاحتمالية العادية للأجزاء الحقيقية والخيالية لطيف الضوضاء الغاوسية.{#fig.noise1 height=“2.5in”}

بدأنا بضوضاء موحدة غير مرتبطة (UU) وأظهرنا أنه، نظرًا لأن طيفها لديه طاقة متساوية عند جميع الترددات، فإن ضوضاء UU هي في المتوسط بيضاء.

لكن عندما يتحدث الناس عن “الضوضاء البيضاء”، فإنهم لا يقصدون دائمًا ضوضاء UU. في الواقع، غالبًا ما يقصدون ضوضاء غاوسية غير مرتبطة (UG).

يوفر thinkdsp تنفيذًا لضوضاء UG:

class UncorrelatedGaussianNoise(_Noise):

    def evaluate(self, ts):
        ys = np.random.normal(0, self.amp, len(ts))
        return ys

تُعيد np.random.normal مصفوفة NumPy من القيم من توزيع غاوسي، في هذه الحالة بمتوسط 0 وانحراف معياري self.amp. نظريًا، يتراوح نطاق القيم من السالب إلى الموجب اللانهاية، لكننا نتوقع أن تكون حوالي 99% من القيم بين -3 و 3.

شبيه ضجيج UG إلى حد كبير بضجيج UU. الطيف له طاقة متساوية عند جميع الترددات، في المتوسط، لذا فإن UG أيضًا أبيض. وله خاصية مثيرة للاهتمام أخرى: طيف ضجيج UG هو أيضًا ضجيج UG. بشكل أكثر دقة، فإن الأجزاء الحقيقية والتخييلية من الطيف هي قيم غاوسية غير مرتبطة.

لاختبار هذا الادعاء، يمكننا توليد طيف ضجيج UG ثم توليد “مخطط الاحتمالية العادية”، وهو وسيلة رسومية لاختبار ما إذا كانت التوزيعة غاوسية.

    signal = thinkdsp.UncorrelatedGaussianNoise()
    wave = signal.make_wave(duration=0.5, framerate=11025)
    spectrum = wave.make_spectrum()

    thinkstats2.NormalProbabilityPlot(spectrum.real)
    thinkstats2.NormalProbabilityPlot(spectrum.imag)

NormalProbabilityPlot متوفر من thinkstats2، الذي تم تضمينه في المستودع لهذا الكتاب. إذا لم تكن على دراية بمخططات الاحتمالية العادية، يمكنك قراءة المزيد عنها في الفصل الخامس من Think Stats على http://thinkstats2.com.

تظهر الشكل 5.8{reference-type=“ref” reference=“fig.noise1”} النتائج. الخطوط الرمادية تظهر نموذجًا خطيًا مناسبًا للبيانات؛ والخطوط الداكنة تظهر البيانات.

تشير الخط المستقيم في مخطط الاحتمالية العادية إلى أن البيانات تأتي من توزيع غاوسي. باستثناء بعض التغيرات العشوائية عند الأطراف، فإن هذه الخطوط مستقيمة، مما يدل على أن طيف ضجيج UG هو ضجيج UG.

طيف ضوضاء UU هو أيضًا ضوضاء UG، على الأقل تقريبًا. في الواقع، وفقًا لنظرية الحد المركزي، فإن طيف أي ضوضاء غير مرتبطة تقريبًا يكون Gaussian، طالما أن التوزيع له متوسط و انحراف معياري محدود، وعدد العينات كبير.

التمارين

توجد حلول هذه التمارين في chap04soln.ipynb.

::: تمرين تمرين 5.1. “همس ناعم” هو موقع ويب يعرض مزيجًا من مصادر الضوضاء الطبيعية، بما في ذلك المطر، والأمواج، والرياح، وما إلى ذلك. في http://asoftmurmur.com/about/ يمكنك العثور على قائمة التسجيلات الخاصة بهم، معظمها في http://freesound.org.

قم بتنزيل بعض من هذه الملفات واحسب طيف كل إشارة. هل يبدو طيف الطاقة مثل الضوضاء البيضاء، أو الضوضاء الوردية، أو الضوضاء البراونية؟ كيف يتغير الطيف مع مرور الوقت؟ :::

::: exercise تمرين 5.2. في إشارة الضوضاء، يتغير مزيج الترددات مع مرور الوقت. على المدى الطويل، نتوقع أن تكون الطاقة عند جميع الترددات متساوية، ولكن في أي عينة، تكون الطاقة عند كل تردد عشوائية.

لتقدير متوسط الطاقة على المدى الطويل عند كل تردد، يمكننا تقسيم إشارة طويلة إلى مقاطع، حساب طيف الطاقة لكل مقطع، ثم حساب المتوسط عبر المقاطع. يمكنك قراءة المزيد عن هذا الخوارزم في http://en.wikipedia.org/wiki/Bartlett’s_method.

قم بتنفيذ طريقة بارتليت واستخدمها لتقدير طيف الطاقة لموجة ضوضاء. تلميح: انظر إلى تنفيذ make_spectrogram. :::

::: exercise تمرين 5.3. في http://www.coindesk.com يمكنك تنزيل سعر بيتكوين اليومي كملف CSV. اقرأ هذا الملف واحسب طيف أسعار بيتكوين كدالة للوقت. هل يشبه الضوضاء البيضاء أو الوردية أو البراونية؟ :::

::: exercise تمرين 5.4. عداد جايجر هو جهاز يكشف عن الإشعاع. عندما تصطدم جسيمات مؤينة بالكاشف، فإنه يخرج دفعة من التيار. يمكن نمذجة الإخراج الكلي في نقطة زمنية كضوضاء بواسون غير مرتبطة (UP)، حيث كل عينة هي كمية عشوائية من توزيع بواسون، والذي يت correspond إلى عدد الجسيمات المكتشفة خلال فترة زمنية معينة. :::

اكتب فئة تسمى UncorrelatedPoissonNoise التي ترث من thinkdsp._Noise وتوفر دالة evaluate. يجب أن تستخدم np.random.poisson لتوليد قيم عشوائية من توزيع بواسون. معامل هذه الدالة، lam، هو متوسط عدد الجسيمات خلال كل فترة. يمكنك استخدام السمة amp لتحديد lam. على سبيل المثال، إذا كانت سرعة الإطار 10 كيلو هرتز وamp هو 0.001، نتوقع حوالي 10 “نقرات” في الثانية.

قم بتوليد حوالي ثانية من ضوضاء UP واستمع إليها. بالنسبة للقيم المنخفضة لـ amp، مثل 0.001، يجب أن تبدو مثل عداد غيغار. بالنسبة للقيم الأعلى، يجب أن تبدو مثل الضوضاء البيضاء. احسب وارسم طيف الطاقة لرؤية ما إذا كان يبدو مثل الضوضاء البيضاء. :::

::: exercise تمرين 5.5. الخوارزمية في هذا الفصل لتوليد الضوضاء الوردية بسيطة من الناحية المفاهيمية ولكنها مكلفة حسابياً. هناك بدائل أكثر كفاءة، مثل خوارزمية فوس-ماكارتني. ابحث عن هذه الطريقة، نفذها، احسب طيف النتيجة، وتأكد من أن لديها العلاقة المرغوبة بين الطاقة والتردد. :::

الارتباط الذاتي