بيئة جيل الصوت والموسيقى في Unity3D. الجزء 2. إنشاء المسار 2D من الموسيقى

الشرح


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



مقدمة


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


هنا خريطة الطريق لهذا اليوم:


  1. النظر في ما هو تقدير.
  2. تعرف على البيانات التي يمكننا الحصول عليها من Audio Clip Unity
  3. فهم كيف يمكننا العمل مع هذه البيانات.
  4. تعرف على ما يمكننا توليده من هذه البيانات.
  5. تعرف على كيفية جعل اللعبة خارج كل هذا (جيدًا ، أو شيء مشابه للعبة)

لذلك دعونا نذهب!


تقديرية النظير السنهالية


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


يشير الحرف T إلى فترة أخذ العينات. كلما كانت الفترة أقصر ، كلما كان تحويل الإشارة أكثر دقة. لكن غالبًا ما يتحدثون عن معكوس: معدل العينة (من المنطقي أن يكون F = 1 / T). 8000 هرتز تكفي لإغناء الهاتف ، وعلى سبيل المثال ، يتطلب أحد خيارات تنسيق DVD-Audio تردد أخذ عينات يبلغ 192،000 هرتز. المعيار في التسجيل الرقمي (في محرري الألعاب ، ومحرري الموسيقى) هو 44 100 هرتز - وهذا هو تردد CD Audio.


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



تقديم موجة الصوت (ثابت)


المعلومات الأساسية


الشكل الموجي (أو الشكل الصوتي ، والعامة - "الأسماك") هو تمثيل مرئي للإشارة الصوتية مع مرور الوقت. يمكن أن يوضح لنا الشكل الموجي عند أي نقطة في الصوت تحدث الطور النشط ، وأين يحدث التوهين. غالبًا ما يتم تقديم الشكل الموجي لكل قناة على حدة ، على سبيل المثال ، مثل هذا:



تخيل أن لدينا بالفعل مصدر صوت وبرنامج نصي نعمل فيه. دعونا نرى ما يمكن أن تعطينا الوحدة.


//  AudioSource    AudioSource myAudio = gameObject.GetComponent<AudioSource>(); //     .     44100. int freq = myAudio.clip.frequency; 

حدد عدد التقارير


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



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



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


التحضير لاستقبال الصوت


دعنا نحدد دقة عينة لدينا على أنها الجودة ، والعدد النهائي للتقارير على أنها sampleCount.


 int quality = 100; int sampleCount = 0; sampleCount = freq / quality; 

مثال على حساب جميع الأرقام سيكون أدناه.


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


 public bool GetData(float[] data, int offsetSamples); 

تأخذ هذه الطريقة صفيفًا تكتب فيه العينات. offsetSamples - المعلمة المسؤولة عن نقطة بداية قراءة صفيف البيانات. إذا قرأت الصفيف من البداية ، فيجب أن يكون هناك صفر.


لتسجيل العينات ، نحن بحاجة إلى إعداد مجموعة لهم. على سبيل المثال ، مثل هذا:


 float[] samples; float[] waveFormArray; //      samples = new float[myAudio.clip.samples * myAudio.clip.channels]; 

لماذا ضربنا طول عدد القنوات؟ الآن سأقول ...


معلومات قناة الصوت الوحدة


يعرف الكثير من الناس أننا في الصوت نستخدم عادة قناتين: اليسار واليمين. يعلم أحدهم أن هناك 2.1 نظامًا ، فضلاً عن 5.1 ، 7.1 التي تحيط بها مصادر الصوت من جميع الجوانب. تم وصف موضوع القنوات جيدًا على الويكي . كيف يعمل هذا في الوحدة؟


عند تنزيل ملف ، عند فتح مقطع ، يمكنك العثور على الصورة التالية:



يظهر فقط هنا أن لدينا قناتين ، ويمكنك أن تلاحظ أنهما مختلفتان عن بعضهما البعض. الوحدة تسجل عينات من هذه القنوات واحدة تلو الأخرى. اتضح هذه الصورة:
[L1،R1،L2،R2،L3،R3،L4،R4،L5،R5،L6،R6،L7،R7،L8،R8...]


لهذا السبب نحتاج إلى ضعف مساحة الصفيف أكثر من عدد العينات.


إذا حددت خيار Force To Mono clip ، فستكون القناة واحدة وستكون جميع الأصوات في الوسط. سوف تتغير معاينة الموجة الخاصة بك على الفور.




تلقي البيانات الصوتية


إليك ما نحصل عليه:


 private int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples,0); //  ,    .       waveFormArray = new float[(samples.Length / sampleCount)]; //             for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { //Abs     ""    . .  waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount; } } 

توتال ، إذا كان المسار يمضي 10 ثوانٍ وكان ذا قناتين ، فسنحصل على ما يلي:


  • عدد العينات في المقطع (myAudio.clip.sample) = 44100 * 10 = 441000
  • مجموعة عينات للقناتين طويلة (samples.Length) = 441000 * 2 = 882000
  • عدد التقارير (sampleCount) = 44100/100 = 441
  • طول الصفيف النهائي = samples.Length / sampleCount = 2000

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


تقديم معلومات الصوت


قم بإنشاء مسار صوتي بسيط باستخدام أدوات Debug


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


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


 for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * .01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * .01f, -waveFormArray[i] * 10, 0); } 

بعد ذلك ، فقط استخدم Debug.DrawLine لرسم متجهاتنا. أي لون يمكن أن تختار. يجب استدعاء كل هذه الطرق في التحديث ، لذلك سنقوم بتحديث المعلومات في كل إطار.


 Debug.DrawLine(upLine, downLine, Color.green); 

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


 private float debugLineWidth = 5; // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); 

المجموع ، هنا هو السيناريو لدينا:


 using UnityEngine; public class WaveFormDebug : MonoBehaviour { private readonly int quality = 100; private int sampleCount = 0; private int freq; private readonly float debugLineWidth = 5; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>(); //  freq = myAudio.clip.frequency; sampleCount = freq / quality; //  samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples, 0); //       waveFormArray = new float[(samples.Length / sampleCount)]; for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount; } } private void Update() { for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * 0.01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * 0.01f, -waveFormArray[i] * 10, 0); // Debug  Debug.DrawLine(upLine, downLine, Color.green); } // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); } } 

وهنا النتيجة:



أنشئ ملفًا صوتيًا سلسًا باستخدام PolygonCollider2D


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


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

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


لذلك ، نحن بحاجة إلى جعل PolygonCollider2D باستخدام بياناتنا. هذا سهل القيام به. يحتوي PolygonCollider2D على حقل نقاط عامة يقبل Vector2 []. أولاً ، نحتاج إلى نقل نقاطنا إلى متجهات النوع المطلوب. دعنا نجعل وظيفة لترجمة مجموعة عيناتنا إلى مجموعة ناقلات:


 private Vector2[] CreatePath(float[] src) { Vector2[] result = new Vector2[src.Length]; for (int i = 0; i < size; i++) { result[i] = new Vector2(i * 0.01f, Mathf.Abs(src[i] * lineScale)); } return result; } 

بعد ذلك ، ما عليك سوى تمرير مجموعة المتجهات الناتجة إلى المصادم:


 path = CreatePath(waveFormArray); poly.points = path; 

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



لدينا شكل صوتي حاد للغاية ، لذلك المسار يخرج غريب. تحتاج إلى سلاسة ذلك. هنا نستخدم خوارزمية المتوسط ​​المتحرك. يمكنك قراءة المزيد حول هذا الموضوع على Habr ، في مقالة The Moving Average Algorithm (Simple Moving Average) .


في الوحدة ، يتم تنفيذ الخوارزمية على النحو التالي:


 private float[] MovingAverage(int frameSize, float[] data) { float sum = 0; float[] avgPoints = new float[data.Length - frameSize + 1]; for (int counter = 0; counter <= data.Length - frameSize; counter++) { int innerLoopCounter = 0; int index = counter; while (innerLoopCounter < frameSize) { sum = sum + data[index]; innerLoopCounter += 1; index += 1; } avgPoints[counter] = sum / frameSize; sum = 0; } return avgPoints; } 

نقوم بتعديل إنشاء مسارنا:


 float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; 

جارٍ التحقق ...



الآن يبدو مسارنا طبيعيًا. استخدمت نافذة عرض 10. يمكنك تعديل هذه المعلمة لاختيار تجانس التي تحتاج إليها.


هنا النص الكامل لهذا القسم:


 using UnityEngine; public class WaveFormTest : MonoBehaviour { private const int frameSize = 10; public int size = 2048; public PolygonCollider2D poly; private readonly int lineScale = 5; private readonly int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private Vector2[] path; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples, 0); waveFormArray = new float[(samples.Length / sampleCount)]; for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount * 2; } //  ,    frameSize float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; } private Vector2[] CreatePath(float[] src) { Vector2[] result = new Vector2[src.Length]; for (int i = 0; i < size; i++) { result[i] = new Vector2(i * 0.01f, Mathf.Abs(src[i] * lineScale)); } return result; } private float[] MovingAverage(int frameSize, float[] data) { float sum = 0; float[] avgPoints = new float[data.Length - frameSize + 1]; for (int counter = 0; counter <= data.Length - frameSize; counter++) { int innerLoopCounter = 0; int index = counter; while (innerLoopCounter < frameSize) { sum = sum + data[index]; innerLoopCounter += 1; index += 1; } avgPoints[counter] = sum / frameSize; sum = 0; } return avgPoints; } } 

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


ربما تساءل الكثيرون منكم ، عند النظر إلى لقطات الشاشة ، عن كيفية رسم المسار بنفسه؟ بعد كل شيء ، المصادمات غير مرئية.


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


الانتهاء


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


  1. ربط سرعة الجهاز إلى مسار BPM. يمكن للاعب التحكم فقط في إمالة السيارة ، ولكن ليس في السرعة. عندها ستشعر الموسيقى أقوى خلال الدورة.
  2. جعل كاشف قليلا وإضافة العروض الخاصة. الآثار التي ستعمل تحت إيقاع. بالإضافة إلى ذلك ، يمكنك إضافة الرسوم المتحركة إلى جسم السيارة ، والتي سوف ترتد على إيقاع النغمة. كل هذا يتوقف على خيالك.
  3. بدلاً من نقل المتوسط ​​، تحتاج إلى معالجة المسار بكفاءة أكبر والحصول على مجموعة من البيانات بحيث لا تختفي القمم ، ولكن كان من السهل إنشاء أثر.
  4. حسنًا ، وبالطبع ، عليك أن تجعل هذه اللعبة ممتعة. يمكنك وضع قطعة نقدية على كل ضربة ، وإضافة مناطق الخطر ، إلخ.

سوف ندرس كل هذا وأكثر من ذلك بكثير في الأجزاء المتبقية من هذه السلسلة من المقالات. شكرا لكم جميعا على القراءة!

Source: https://habr.com/ru/post/ar432134/


All Articles