PyDA Course
متقدم حرّ ⏱ 120 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

ملحن الموسيقى بالذكاء الاصطناعي

CreativeAudioMachine Learning

تأليف لحن من لا شيء مشكلة صفحة بيضاء؛ تأليف تنويع للحن يعجبك بالفعل مشكلة إحصاءات. يبني هذا المشروع النوع الثاني من الملحن: يقرأ لحنًا بذرة قصيرًا، ويتعلم كيف تميل كل نغمة إلى تتبع سابقتها، ثم يولّد ألحانًا جديدة من ذلك النموذج المتعلّم، يكدّس الأكوردات تحتها، ويصدّر النتيجة كملف MIDI حقيقي ، ملف أغنية يمكنك فتحه في أي مشغّل أو محطة عمل صوتية رقمية. «الذكاء الاصطناعي» هنا أنيق وأمين: سلسلة ماركوف (Markov chain)، التي ليست أكثر من «بناءً على ما سمعته حتى الآن، أي نغمة تأتي عادة بعد ذلك؟».

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

🎯 ما ستفعله

  1. نمذجة طبقة الصوت (pitch) كأرقام نوتات MIDI والإيقاع كمدة بالأزمنة الإيقاعية ، محوّلًا الموسيقى إلى قائمة Python من الأرقام.
  2. بناء سلسلة ماركوف من لحن بذرة والتحقق مما تعلّمته بقراءة جدول انتقالاته.
  3. توليد لحن جديد بأي طول بالمشي عبر تلك السلسلة.
  4. اشتقاق أكوردات ثلاثية (triads) من درجات السلم ووضعها تحت اللحن.
  5. تصدير مقطوعة كاملة إلى ملف .mid حقيقي بـ midiutil، والتحقق من الملف على القرص.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الواضح الموصى به ، ناتج هذا المشروع ملف .mid على قرصك الخاص ستريد فتحه في مشغّل محلي، والاعتماد (midiutil) على بُعد أمر uv add واحد.

GitHub Codespaces يعمل جيدًا أيضًا: افتح مستودع الدورة كاملًا في Codespace مجاني وكل خطوة أدناه تعمل دون تغيير؛ .mid المولَّد أثر قابل للتنزيل يمكنك التقاطه من شجرة الملفات.

Google Colab وKaggle Notebooks وBinder طريقة حقيقية لتشغيل كل خطوة, لأن المولّد نفسه Python خالص زائد مكتبة واحدة قابلة للتثبيت عبر pip (!pip install midiutil). التحفظ الصادق: نظام ملفات دفتر الملاحظات مؤقت، فالملف .mid الذي تصدّره يعيش هناك ، نزّله قبل إغلاق الجلسة. وليس هناك مخرجات صوتية في دفتر ملاحظات، لذا ستريد مع ذلك سحب الملف محليًا لتسمع النتيجة فعلًا.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما يلزم قبل التوليد: مشروع بـ midiutil, ومرتكز ذهني لمعنى رقم نوتة MIDI.

أعِدَّ المشروع

uv init ai-music-composer
cd ai-music-composer
uv add midiutil

midiutil مكتبة صغيرة موثوقة تحوّل كائنات Python إلى ملف .mid ثنائي ، الصيغة المحمولة نفسها التي يستطيع كل DAW وتطبيق هاتف ومشغّل وسائط فتحها. الملحن نفسه لن يعتمد عليها حتى الخطوة الأخيرة؛ كل شيء آخر مكتبة Python القياسية.

مرِّس لنفسك: النوتة ← رقم

👟 تلميح البداية :

اطبع قاموسًا صغيرًا نوتة←رقم حتى يعني كل رقم لاحق في هذا المشروع شيئًا موسيقيًا بدلًا من التعسف.

# composer.py
from collections import defaultdict
import random

NOTES = ["C", "D", "E", "F", "G", "A", "B"]
SEMITONES = [0, 2, 4, 5, 7, 9, 11]  # semitones of the major scale steps

MIDI: dict[str, int] = {}
for octave in range(3, 6):
    for i, name in enumerate(NOTES):
        MIDI[f"{name}{octave}"] = 60 + 12 * (octave - 4) + SEMITONES[i]

print(MIDI["C4"], MIDI["E4"], MIDI["A4"])

أرقام MIDI أنصاف نغمات معروضة من أسفل لوحة المفاتيح، والوسط C به 60. بناء الجدول من خطوات السلم الكبير [0, 2, 4, 5, 7, 9, 11] ، أي كامل-كامل-نصف-كامل-كامل-كامل-نصف، النمط نفسه كالمفاتيح البيضاء للبيانو ، يعني أن كل اسم في الجدول طبقة صوت مشروعة في دو الكبير منذ البداية.

✅ قائمة التحقق

  • ✅ ينتهي uv add midiutil دون أخطاء.
  • ✅ يطبع MIDI["C4"] 60, وMIDI["E4"] 64, وMIDI["A4"] 69 ، كلٌّ منها على بُعد أربعة أنصاف نغمات صعودًا من آخر قفزة تتوقعها.
  • ✅ يحمل composer.py جدول النوتة-الرقم والاستيرادين من المكتبة القياسية في أعلاه.

الخطوة 1: حوّل لحنًا إلى أرقام

النُّوتة المدرجة (score) نثر؛ وسلسلة ماركوف تحتاج بيانات. تحوّل هذه الخطوة لحن بذرة صغيرًا ، يمكنك أن تُهدهده ، إلى قائمة مسطحة من أرقام MIDI، وتُدخل وجهة النظر «نغمة تتبع نغمة» التي بُني عليها الملحن كله.

1.1 اكتب البذرة كقائمة قيم MIDI

👟 تلميح البداية :

انسخ البذرة الكلاسيكية C4 D4 E4 D4 C4 E4 F4 G4 A4 G4 F4 E4 D4 C4 (العبارة الأولى من نشيد نوم) إلى قائمة من الأرقام من جدول MIDI.

# composer.py (continued)
SEED = [MIDI["C4"], MIDI["D4"], MIDI["E4"], MIDI["D4"], MIDI["C4"], MIDI["E4"],
        MIDI["F4"], MIDI["G4"], MIDI["A4"], MIDI["G4"], MIDI["F4"], MIDI["E4"],
        MIDI["D4"], MIDI["C4"]]

print(SEED)

اللحن تسلسل، والتسلسلات هي مدخل سلاسل ماركوف. جعل البذرة قائمة أرقام بدلًا من أسماء نوتات هو التجريد الأساسي: المولّد لا يحتاج قط إلى معرفة كيف «يبدو» 64, فقط أنه كثيرًا ما يتبع 62.

🎯 الناتج المتوقع :

[60, 62, 64, 62, 60, 64, 65, 67, 69, 67, 65, 64, 62, 60] ، 14 نغمة، تبدأ وتنتهي على 60.

🩹 إذا لم يعمل :

إذا بدا رقم خطأً، تحقق من الأوكتاف في بناء جدول MIDI (قيمة C4 غير 60 تعني انحراف الإزاحة (octave - 4)). إذا كانت للقائمة مشاكل في الطول، فعدّ الأقواس ، يجب ألّا يضيف التفاف السطر قيمةً أو يُسقطها.

1.2 قسّم اللحن إلى ملاحظات (observations)

👟 تلميح البداية :

اقرن كل نغمة بخليفتها ، zip(SEED, SEED[1:]) ، وتأكد أن الملاحظات تُقرأ نغمة ← نغمة.

# composer.py (continued)
observations = list(zip(SEED, SEED[1:]))
print(observations[:4])
print("made", len(observations), "pairs from", len(SEED), "notes")

zip(a, a[1:]) هو النمط الذي يفصل أي تسلسل إلى أزواج متجاورة ، لاحظ أنه ينتج بالضبط len(SEED) - 1 زوجًا، لأن النغمة الأخيرة لا خليفة لها. كل زوج وحدة «قواعد» موسيقية: بمعرفة 62، لاحظت 64.

🎯 الناتج المتوقع :

[(60, 62), (62, 64), (64, 62), (62, 60)] والعدد made 13 pairs from 14 notes.

🩹 إذا لم يعمل :

إذا أظهرت الأزواج قيمًا ليست في SEED، فقد زرجت البنية الخطأ (SEED[:-1] وSEED[1:] هجاء أنفع من الشريحة المختلطة). إذا ساوى عدد الأزواج عدد النوتات، فشريحة انعكست ، يجب أن يكون عدد الأزواج أقل بواحد من النوتات.

1.3 تحقّق

✅ قائمة التحقق

  • ✅ تنتج نوتات البذرة الأربع عشرة كلها 13 زوجًا متجاورًا.
  • ✅ العنصر الثاني في كل زوج هو النغمة التالية في البذرة الأصلية.
  • ✅ تستطيع ترجمة SEED[5] إلى اسم نوتة يدويًا دون تشغيل كود.

🤔 سؤال (أسئلة) سقراطي(ة)

  • البذرة في دو الكبير وكل قيمة تبقى في أوكتاف واحد. ماذا يتغير في أزواج الملاحظات لو نقلت البذرة كلها أوكتافًا للأعلى ، البنية، أم الأرقام فقط؟ وماذا يقول ذلك عن مكان «الموسيقى»؟
  • يقرن zip نوتات متجاورة بدقة، متجاهلًا مدة الإمساك بكل نغمة. أي صفة موسيقية حقيقية ، شكل العبارة مثلًا ، غير مرئية لهذا النموذج، وأين تتوقع أن تظهر أولًا في هذا المشروع؟

الخطوة 2: تعلّم سلسلة ماركوف

تجيب سلسلة ماركوف عن سؤال واحد لكل نغمة: «بمعرفة النغمة الحالية، ماذا يقول البيانات عن الأرجح في التالي؟» تخزّن نسخة الملحن كل خليفة ملحوظ لكل نغمة في defaultdict من القوائم ، رخيص وشفاف وقابل للفحص، تمامًا كجدول ترددات يمكنك قراءته.

2.1 ابنِ جدول الانتقالات

👟 تلميح البداية :

اكتب build_chain(sequence) تُعيد {note: [successors]} باستخدام defaultdict(list), ثم اطبع صف نغمة واحدة.

# composer.py (continued)
from collections import defaultdict

def build_chain(sequence: list[int]) -> dict[int, list[int]]:
    chain: dict[int, list[int]] = defaultdict(list)
    for current, nxt in zip(sequence, sequence[1:]):
        chain[current].append(nxt)
    return chain

chain = build_chain(SEED)
print("after 64:", chain[64])

يقول chain[current].append(nxt) «عندما رأيت current آخر مرة، تبعتها هذه المرة nxt». التكرار على الأزواج مرة واحدة يبني النموذج كله ، الجدول، في الواقع، توزيع ترددات لكل نغمة، وdefaultdict(list) يعني أنك لن تحتاج أبدًا إلى حالة خاصة لنغمة تظهر أول مرة.

🎯 الناتج المتوقع :

after 64: [62, 65, 62] ، تبع 64 في البذرة بـ62 أول مرة قرب البداية، وبـ65 في الصعود نحو الذروة، وبـ62 مرة أخرى في النزول.

🩹 إذا لم يعمل :

إذا كان الصف [] أو مفقودًا، فلم يظهر 64 كأي نغمة حالية ، تحقق أنك تبني من SEED, لا من قائمة فارغة. إذا أدرج صف خلفاء خاطئين بشكل صارخ، فـzip في build_chain يقرن الجيران الخطأ ، اطبع list(zip(sequence, sequence[1:]))[:3] وقارنه بالبذرة.

2.2 أضف العشوائية ببذرة (seed)

👟 تلميح البداية :

أكمل generate_melody(chain, start, length) ، امشِ في السلسلة، وعندما لا تسجّل نغمة أي خليفة، ارجع إلى نغمة البداية بدلًا من التعطل.

# composer.py (continued)
def generate_melody(chain: dict[int, list[int]], start: int, length: int) -> list[int]:
    melody = [start]
    current = start
    for _ in range(length - 1):
        successors = chain[current]
        current = random.choice(successors) if successors else start
        melody.append(current)
    return melody

random.seed(7)
print(generate_melody(chain, MIDI["C4"], 8))

random.choice هو ما يجعل كل تشغيل ملحنًا لا مُسجِّلًا ، السلسلة تعطي الأبجدية (أي النوتات قد تتبع)، والصدفة تختار داخلها. ارتداد if successors else start هو صمام الأمان لنوتات أنهت عبارات فقط (كـC4 الأخير، الذي لا خليفة له في البذرة).

🎯 الناتج المتوقع :

قائمة طولها 8 تبدأ بـ60, عناصرها اللاحقة كلها مسحوبة من حِيز خلفاء chain ، مع random.seed(7) ناتج هذا المشروع قابل لإعادة الإنتاج، لكن غيّر البذرة ويتغير اللحن بشرعية.

🩹 إذا لم يعمل :

إذا ظهر KeyError: ...، فوصلت نغمة نهاية اللحن دون ارتداد ، جملة else start مفقودة أو تُتخطى لأنك فهرست chain[current] بـ[] بدلًا من .get. إذا لم يغادر الناتج نغمة واحدة أبدًا، فيحل successors قائمة فارغة باستمرار، ومعنى ذلك أن السلسلة بُنيت من مدخل خاطئ.

2.3 تحقّق

✅ قائمة التحقق

  • ✅ ينتج build_chain(SEED) صفًا واحدًا لكل نغمة مميزة، ويدرج كل صف فقط نوتات تبعتها فعلًا في البذرة.
  • ✅ يعمل generate_melody ببذرة عشوائية ثابتة وبأخرى متغيرة مرارًا.
  • ✅ كل نغمة مولّدة نغمة يمكن أن تنتجها السلسلة شرعيًا، لا طبقة صوت مخترعة أبدًا.

🤔 سؤال (أسئلة) سقراطي(ة)

  • السلسلة تتحرك للأمام نغمة واحدة فقط ، لا ذاكرة لـ«نغمتين مضتا». أي نسيج موسيقي سيكون مرئيًا لسلسلة من الدرجة الثانية (مفتاحية على الأزواج) يعمى عنه النموذج الحالي؟
  • random.seed(7) يجعل الناتج قابلًا لإعادة الإنتاج. ما خطر ملحن يتظاهر أن كل تشغيل يجب أن يختلف ، وماذا تشتري لك قابلية إعادة الإنتاج عندما تحاول إصلاح لحن أعجبك من تشغيل سابق؟

الخطوة 3: امنح اللحن إيقاعًا

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

3.1 نمذجة الإيقاع كمدة إيقاعات

👟 تلميح البداية :

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

# composer.py (continued)
def make_phrase(melody: list[int], durations: list[float]) -> list[tuple[int, float]]:
    return list(zip(melody, durations))

phrase = make_phrase(SEED, [1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0,
                            0.5, 0.5, 1.0, 1.0, 1.0, 2.0])
print(phrase[:3])
print("phrase spans", sum(d for _n, d in phrase), "beats")

تُقاس المدد بالإيقاعات، وهي الوحدة التي تخزنها ملفات MIDI فعلًا: 0.5 ثمن، 1.0 ربع، 2.0 نغمة نصف. يعيد zip بناء لحن إلى قائمة أحداث (pitch, beats) دون لمس توليد الطبقة، ويجيب sum كل المدد مباشرة عن سؤال الملحن الواضح ، «كم تبلغ هذه العبارة؟» ، مباشرةً.

🎯 الناتج المتوقع :

[(60, 1.0), (62, 1.0), (64, 0.5)] وphrase spans 13.0 beats (النغمة الأخيرة احتُجزت إيقاعين كاملين).

🩹 إذا لم يعمل :

إذا كانت العبارة بعدد عناصر مختلف عن اللحن، فقائمة المدد بطول مختلف ، يبتاع zip بصمت إلى الأقصر، فتأكد len(durations) >= len(melody) مبكرًا أو يختفي ذيل اللحن. إذا بدا المجموع خاطئًا، تحقق أن مدة 2.0 الأخيرة حطت فعلًا على آخر نغمة.

3.2 كرّر العبارة في بنية أغنية

👟 تلميح البداية :

كرر العبارة بعض المرات و«لحّن» عدّاد أشرطة الأغنية كاملًا، حتى يكون لخطوة التصدير طول فعلي تكتبه.

# composer.py (continued)
def make_song(phrase: list[tuple[int, float]], repeats: int, chain, start: int) -> list[tuple[int, float]]:
    song: list[tuple[int, float]] = []
    for _ in range(repeats):
        melody = generate_melody(chain, start, len(phrase))
        song.extend(make_phrase(melody, [d for _n, d in phrase]))
    return song

song = make_song(phrase, 4, chain, MIDI["C4"])
print(len(song), "notes =", sum(d for _n, d in song), "beats")

إعادة استخدام هيكل الإيقاع نفسه لكل تكرار هي الطريقة الكلاسيكية للتنوع الهيكلي الرخيص: يبقى التوقيت معروفًا بينما تنوّع السلسلة الطبقات. توسيع الأغنية نغمةً بنغمة عبر list.extend يُبقي إجمالي الإيقاعات دقيقًا ، بتكرار أربع مرات، عبارة 13 إيقاعًا تساوي 52 بالضبط.

🎯 الناتج المتوقع :

52 notes = 52.0 beats ، أربع نسخ من العبارة ملصوقة خلف بعضها، كلٌّ بطبقات مولّدة حديثًا (لكن شرعيةِ السلسلة).

🩹 إذا لم يعمل :

إذا كانت الأغنية 14 نغمة بدلًا من 56، فجسم الحلقة بنى عبارة ثم خرج ، تحقق من extend, لا append, حتى تتراكم لا تستبدل. إذا انزاحت الإيقاعات إلى 51 أو 53، أُعيد لحن مولّد بطول مختلف عن len(phrase) واقتطع zip مبكرًا.

3.3 تحقّق

✅ قائمة التحقق

  • ✅ تقرن العبارات الطبقات بمدد الإيقاعات، ومدد المجاميع دقيقة.
  • ✅ تكرر الأغاني متعددة العبارات هيكل الإيقاع بينما تدع سلسلة اللحن تتنوع.
  • ✅ تستطيع التنبؤ بإجمالي عدّاد إيقاعات أغنية من عبارتها وعدد تكراراتها.

🤔 سؤال (أسئلة) سقراطي(ة)

  • كل تكرار يعيد تشغيل generate_melody بنفس len(phrase). ماذا يحدث لطول الأغنية إذا ولّد لحن يومًا ما نغمة أكثر من العبارة ، ولماذا يخفي zip مع إيقاع ثابت ذلك الخطأ تمامًا؟
  • الإيقاع حاليًا مُثبَّت كإيقاع البذرة. أي نغمة في phrase تتوقع أن تحط على إيقاعات قوية مقابل ضعيفة، وما الأثر التركيبي لهذا التأكيد على عبارة تبدأ بنغمة الاستهلال D4؟

الخطوة 4: كدِّس الأكوردات تحتها

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

4.1 ابنِ ثلاثيات من درجات السلم

👟 تلميح البداية :

عرّف scale كقائمة طبقات تمتد أوكتافًا، وtriad(degree) كـ[scale[d], scale[d+2], scale[d+4]] فيصعد الأكورد «المفاتيح البيضاء».

# composer.py (continued)
SCALE = [60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83]  # C4 up to B5

def triad(degree: int) -> list[int]:
    return [SCALE[degree], SCALE[degree + 2], SCALE[degree + 4]]

print(triad(0), triad(5), triad(3), triad(4))

يمتد SCALE أوكتافين كاملين بدقة حتى يظل degree + 4 مشروعًا لكل درجة ، لا حساب التفاف أوكتاف. تكديس ثلاث خطوات سلم متناوبة ينتج رصة الأثلاث الكلاسيكية: C (60,64,67), Am (69,72,76), F (65,69,72), G (67,71,74). قائمة أوكتاف واحد ستقطّع أكوردات عالية كـAm أوكتافًا للأسفل، فالأوكتاف الثاني هو ما يجعل التناغم حقيقيًا.

🎯 الناتج المتوقع :

[60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74] ، ثلاثيات C وA-minor وF وG في دو الكبير.

🩹 إذا لم يعمل :

تحقق من ثلاثي يبدو خاطئًا بعدّ أنصاف النغمات من الجذر ، يجب أن يكون Am 69, 72, 76 (A–C–E). إذا حط كل أكورد في الأوكتاف المنخفض مثل [69, 60, 64], فـSCALE قائمة الأوكتاف الواحد ذات السبع عناصر، فلفّت degree + 2 وdegree + 4 خارج المدى. إذا كان كل أكورد صغيرًا مثل [60, 62, 64], ففهرست [d, d+1, d+2] بدلًا من تخطي كل خطوة سلم.

4.2 اكتب تتابع أكورد يناسب المقطوع

👟 تلميح البداية :

اختر تتابع درجات قصيرًا ، الكلاسيكي I–vi–IV–V = الدرجات [0, 5, 3, 4] ، ووسّعه عبر عبارات الأغنية المتكررة، أكوردًا واحدًا كل إيقاعين.

# composer.py (continued)
def chord_schedule(song: list[tuple[int, float]], progression: list[int]) -> list[list[int]]:
    chords: list[list[int]] = []
    beat = 0.0
    for _n, dur in song:
        degree = progression[int(beat) // 2 % len(progression)]
        chords.append(triad(degree))
        beat += dur
    return chords

chords = chord_schedule(song, [0, 5, 3, 4])
print(chords[0], chords[2], chords[28], chords[55])

int(beat) // 2 يشطر الأغنية إلى نوافذ إيقاعين ، كل نافذة تحمل أكوردًا واحدًا، و% len(progression) يلف التتابع حول طول الأغنية. النتيجة تسميات أكورد لكل نغمة، وهي بالضبط ما سيستهلكه مُصدّر MIDI في الخطوة 5. لاحظ التبسيط الصادق: توزيع حقيقي يحفظ أكوردًا واحدًا لكل بار, هذا المشروع واحدًا لكل إيقاعين، والفرق مسموع تمامًا لمقطوعة تعليمية.

🎯 الناتج المتوقع :

chords[0] هو ثلاثي C [60, 64, 67], وchords[2] (يبدأ عند إيقاع 2.0, النافذة 1) هو Am [69, 72, 76], وchords[5] (يبدأ عند إيقاع 4.0, النافذة 2) هو F [65, 69, 72], وchords[8] (يبدأ عند إيقاع 7.0, النافذة 3) هو G [67, 71, 74] ، منعطف I–vi–IV–V كاملًا في عبارة الأغنية الأولى.

🩹 إذا لم يعمل :

إذا طبع فهرس أكورد ثلاثيًا خارج الأربعة, فلفّ % len(progression) أو نافذة // 2 انحرفا ، أعد الحساب يدويًا لإدخال واحد: chords[8] يبدأ عند إيقاع 7.0, لذا int(7.0) // 2 = 3, 3 % 4 = 3, الدرجة 4, الثلاثي G. إذا كانت كل الأكوردات متطابقة, فـprogression مُرِّرت قائمة عنصر واحد.

4.3 تحقّق

✅ قائمة التحقق

  • ✅ تعطي كل درجة ثلاثيًا من ثلاث نوتات متباعدة بثلث.
  • ✅ يتكرر تتابع [0, 5, 3, 4] بنظافة عبر أغنية كاملة.
  • ✅ كل نغمة في الأغنية لها أكورد مخصص بلا ثغرات.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يتبع الأكورد نافذة إيقاعين ثابتة مهما كان اللحن يفعل. أين في chord_schedule تحقن «غيّر الأكورد فعلًا عندما تحط اللحن على إيقاع قوي» ، وما الازدحام الموسيقي الذي يصلحه ذلك؟
  • تأتي الأكوردات الأربعة كلها من سلم كبير واحد، فكل أكورد «في المفتاح». إذا سمحت بأكورد مستعار (نوتة عرضية واحدة خارج SCALE)، فأين ينهار نموذج الخطوة 2 بصمت، ولماذا لن يشتكي كاتب MIDI؟

الخطوة 5: صدّر إلى ملف MIDI حقيقي

كل شيء حتى الآن يعيش في قوائم Python. تكتب هذه الخطوة تلك القوائم في ملف .mid قابلًا للتشغيل فعلًا بـmidiutil, باستخدام مسارين ، لحن ثم تناغم ، وتتحقق من الملف على القرص حتى تعرف أن التصدير نجح دون حاجة لسماع نغمة.

5.1 ارسم الأغنية في أحداث MIDI

👟 تلميح البداية :

اكتب write_midi(song, chords, filename) بها استدعاء addTempo, ومسار لحن عند الزمن 0, ومسار أكورد يبدأ لاحقًا قليلًا حتى لا يتراكب مع الاستهلال.

# composer.py (continued)
import os
from midiutil import MIDIFile

def write_midi(song: list[tuple[int, float]], chords: list[list[int]], filename: str = "song.mid") -> None:
    midi = MIDIFile(2)  # tracks 0 and 1: melody and chords
    tempo, volume = 120, 96

    melody_time = 0.0
    for note, dur in song:
        midi.addNote(0, 0, note, melody_time, dur, volume)
        melody_time += dur

    chord_time = 0.0
    for chord in chords:
        for note in chord:
            midi.addNote(1, 0, note, chord_time, 2.0, 48)
        chord_time += 2.0

    with open(filename, "wb") as f:
        midi.writeFile(f)

write_midi(song, chords, "song.mid")
print("wrote song.mid in", os.path.getsize("song.mid"), "bytes")

MIDIFile(2) ينشئ المسارين 0 و1 ، لحن على 0, أكوردات على 1 ، وaddTempo(0, 0, 120) يثبّت حدث الإيقاع على المسار القائد نفسه. غرابة تستحق المعرفة: في الصيغة 1 عدّاد المسارات في الترويسة numTracks + 1 لأن midiutil يعد دائمًا ضمنها مسار إيقاع أول، لذا سيُبلّغ الملف نفسه عن 3 مسارات وإن قال المنشئ 2 ، سيؤكد المتحقق في 5.2 ذلك بالضبط. addNote(track, channel, pitch, time, duration, volume) هو سطح الترجمة كله: الطبقة وزمن البدء والطول بالإيقاعات كلها ترتبط واحدًا لواحد من بنى البيانات السابقة. ارتفاع أكورد أقل (48 مقابل 96 للّحن) قرار خلط يُبقي مقطوعة تعليمية من التحول إلى ضجة، وكتابة البايتات بـwriteFile إلى كائن ملف مفتوح هي التصدير كله.

🎯 الناتج المتوقع :

wrote song.mid in <بضعة آلاف> bytes, والملف موجود في مجلد المشروع، قابلًا للفتح بأي مشغّل أو DAW يدعم MIDI.

🩹 إذا لم يعمل :

إذا ظهر FileNotFoundError أو ملف فارغ, فمسار الكتابة خطأ أو لم يعمل writeFile أبدًا ، تأكد أن سياق open(..., "wb") هو المكان الوحيد الذي يكتب. إذا أبلغ مشغّل عن ملف تالف, فزمن نوتة رجع للخلف (أُسقط += تراكمي) وانهار الخط الزمني للمسار.

5.2 تحقق أن الملف أغنية فعلًا

👟 تلميح البداية :

تلصّص داخل بايتات .mid الخام ، سحر الترويسة MThd, وحقل عدّاد المسارات في الترويسة, وعدّاد بايتات حالة تشغيل-نوتة ، لتأكيد أن التصدير ملف MIDI حقيقي منظم لا بايتات عشوائية تلبس امتداد .mid.

# composer.py (continued)
def verify(midi_path: str = "song.mid") -> None:
    with open(midi_path, "rb") as f:
        data = f.read()
    n_tracks = int.from_bytes(data[10:12], "big")  # 'ntrks' header field
    note_ons = sum(1 for byte in data if (byte & 0xF0) == 0x90)
    print("is a MIDI file:", data[:4] == b"MThd")
    print("track count (header):", n_tracks)
    print("note-on events:", note_ons)

verify()

كل ملف MIDI قياسي يفتح بسحر MThd رباعي البايتات, لذا data[:4] هو الفحص الواحد الذي يفصل .mid حقيقيًا عن ملف نصي معاد تسميته. MIDI بروتوكول على مستوى البايت: بايت حالة في مدى 0x90–0x9F هو رسالة تشغيل-نوتة, فمسح data بـ(byte & 0xF0) == 0x90 يعد بالضبط النوتات التي كتبتها. البايتات 10–12 من الترويسة عدّاد المسارات, وتقرأ 3 لأن الصيغة 1 تعد مسار إيقاع قائدًا فوق مساريك (numTracks + 1).

🎯 الناتج المتوقع :

is a MIDI file: True, track count (header): 3 (إيقاع + لحن + أكوردات), وnote-on events: 224 ، len(song) للّحن زائد 3 * len(chords) للأكوردات (56 + 168).

🩹 إذا لم يعمل :

إذا فشل فحص الترويسة, فالملف ليس ملف MIDI ، تحقق مما كُتب تحت هذا الاسم. إذا كان note-on events قصيرًا, فأسقطت نوتات الأكوردات أو اللحن وقت الكتابة; وإذا كان أطول من المتوقع, فانزلق بايت 0x90-كحالة من حدث إيقاع أو تغيير برنامج, وعدّاد مسارات الترويسة هو الحقيقة المرجعية الأوثق. إذا لم يكن عدّاد المسارات 3, فاستخدم التصدير MIDIFile(...) بحجم مختلف عن الذي يفترضه القارئ.

5.3 تحقّق

✅ قائمة التحقق

  • ✅ يوجد song.mid, يبدأ بـMThd, يُبلّغ عن 3 مسارات في ترويسته, ويمسح إلى أحداث تشغيل-نوتة 224 المتوقعة.
  • ✅ مسارا اللحن والأكورد منفصلان ، وعدد أحداثهما يطابق البنيتين اللتين أنتجتهما الخطوتان 3 و4.
  • ✅ امتداد إيقاعات اللحن يساوي إجمالي إيقاعات الأغنية المحسوب.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تخزن مواصفة MIDI الزمن في ticks لكل ربع; تختار midiutil دقة لك. أي تغيير في جهة الاستيراد ، دقة ticks لمحطة DAW مختلفة مثلًا ، يمكن أن يجعل إيقاع المقطوع يبدو منحرفًا رغم أن addTempo يقول 120؟
  • 224 حدث نوتة كثير من الكتابات لبيانات مضبوطة يدويًا. كيف تتغير دالة chord_schedule لو أردت إسقاط مسار الأكوردات كليًا لسطر منفرد ، وماذا يكشف جوابك عن درجة ترابط المسارين وقت write_midi؟

⚠️ مآزق شائعة

  • نسيان أن النوتة 60 هي الوسط C. بناء جدول MIDI بإزاحة (octave - 4) خاطئة ينتج ملحنًا مشروعًا تمامًا يكتب كل شيء أوكتافًا منحرفًا ، ولن يشتكي MIDI, أذناك فقط.
  • zip يبتاع بصمت. make_phrase(melody, durations) بأطوال غير متطابقة يُسقط نوتات دون خطأ. أضف فحص طول صريحًا حين تعلّم الملحن قرن الطبقات بالتوقيت.
  • سلسلة بلا ارتداد لنوتات النهاية. لا خليفة لـC4 الأخير في البذرة; دون if successors else start, يلقي المولّد KeyError على اللحن نفسه الذي يفترض توسيعه.
  • نوافذ أكورد متداخلة. إذا أُعطي أكورد مدةً أطول من نافذة إيقاعيه, تمشي أحداث الأكورد إلى النافذة التالية وتتحول المقطوع إلى خليط ، أبقِ مدة الأكورد مضاعفًا دقيقًا لحجم النافذة.
  • تصدير دون التحقق من الترويسة. .mid ليس ملف MIDI فعلًا (بلا MThd) سيبدو «منجزًا» في شجرة الملفات ويفشل في كل مكان آخر. فحص البايتات الأربع هو التحقق الرخيص الوحيد الذي يلتقطه.

ما بنيته للتو

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

شغّل نسخة أكمل دون أي إعداد محلي

examples/ai-music-composer/ في مستودع الدورة هو الملحن كاملًا كدفتر ملاحظات, من البذرة إلى song.mid قابل للتنزيل. استنسخه, أو افتح المستودع كاملًا في GitHub Codespace.

إلى أين تذهب من هنا

  • رقِّ إلى سلسلة من الدرجة الثانية مفتاحية على أزواج (current, previous) ، chain[(60, 62)] ، واسمع كيف تكتسب الألحان عبارات تتكرر فعلًا بدلًا من التجوال فحسب.
  • أضف علم CLI لضبط الإيقاع (--tempo 90) ووسيطة --degree-progression "0 5 3 4" ليكتب الكود نفسه مقطوعات شبيهة بالفالس أو دافعة دون تعديلات.
  • وسّع نموذج الإيقاع إلى سلسلة ماركوف على المدد أيضًا, فيختار المولّد متى تبدأ النوتة كما يختار طبقتها.
  • صدّر خط باس أوكتافًا تحت جذور الأكوردات, ثم طابق المسارات الثلاثة ، أول توزيع متعدد الأنسجة حقًا ينتجه هذا الخط.

شارك مشروعك مع الصف

بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request, حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع, وإنشاء فرع, وتثبيت ملفاتك, وفتح الـ PR, خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة Python خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.