تخطَّ إلى المحتوى

مُصوِّر الصوت

سنتعمق هنا في بعض خصائص الصوت في واجهة API ونعرض لك أساسيات إنشاء مُصوِّر الصوت. تُبنى معظم المُصوِّرات على المجموعة نفسها من المبادئ الأساسية، لذا يمكن لقليل من التدريب أن يُنتج نتائج ممتعة للغاية.

يمكن الوصول إلى بيانات الصوت التي يوفرها SignalRGB من خلال بضع خصائص في شيفرتك:

  • engine.audio.level - تُرجع رقمًا بين -100 و0 يمثل مستوى الصوت الإجمالي للمقطع. القيمة 0 تعني صوتًا عاليًا، و-100 تعني صوتًا منخفضًا جدًا.
  • engine.audio.density - تُرجع رقمًا بين 0 و1 يمثل خشونة النغمة، حيث تُرجع نغمات الاختبار 0 ويُرجع الضجيج الأبيض 1.
  • engine.audio.freq - تُرجع مصفوفة من 200 عنصر تحتوي على بيانات تردد المقطع.

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

لنبدأ بحركة التردد الأساسية.

يمثل التردد درجة حدة الصوت الذي نسمعه، وهو أهم خاصية في مُصوِّرات الصوت. ما نفعله هنا هو أخذ 200 شريحة من موجة التردد في كل إطار وتحويلها إلى شكل مرئي. العملية الأساسية هي:

  • أنشئ مصفوفة واملأها ببيانات التردد.
  • عدّل هذه المصفوفة لتناسب احتياجاتك (filter وmap وreduce وغيرها).
  • اكتب صنفًا (class) باسم “شريط الصوت” لتمثيل كل عنصر.
  • اربط البيانات بصنف شريط الصوت في كل إطار.

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

مثال - بيانات غير معالجة:

بيانات معالجة:

مثال على الشيفرة مع البيانات المعالجة:

<head>
<title>Visualizer Tutorial</title>
<meta description="Basic Effects" />
<meta publisher="SignalRgb" />
</head>
<body style="margin: 0; padding: 0; background: #000;">
<canvas id="exCanvas" width="320" height="200"></canvas>
</body>
<script>
canvas = document.getElementById('exCanvas');
ctx = canvas.getContext('2d');
var effects = [];
var reducedFreq = [];
function update() {
// "frequency" represents the full 200 elements from our frequency data
var frequency = new Int8Array(engine.audio.freq)
// "reducedFreq" filters the data down to every fourth result to save some CPU load
reducedFreq = frequency.filter((element, index) => {
return index % 4 === 0;
})
// Create the effect if it does not yet exist
if(effects.length < 1){
effects.push(new soundBars(20, 100))
}
// Background color
DrawRect(0, 0, 320, 200, "black")
// Play effect
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime <= 0) {
effects.splice(i, 1);
}
});
window.requestAnimationFrame(update);
}
function soundBars (x, y){
this.x = x;
this.y = y;
this.draw = function(){
for(let i = 0; i < reducedFreq.length; i++){
var x = this.x + 5 * i
var y = this.y
// Data processing occurs for "height". Find the absolute value of each element, then flip negative
var height = -Math.abs(reducedFreq[i])
DrawRect(x, y, 5, height, "white")
}
}
}
function DrawRect(x, y, width, height, color) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
};
window.requestAnimationFrame(update);
</script>

ومع ذلك، لا تزال هناك بعض المشكلات في المُصوِّر أعلاه. فعلى الرغم من أن الأغنية تبدو متوازنة عبر سماعات الأذن، يمكننا أن نرى أن البيانات تميل بشدة لصالح بعض أشرطة الصوت وغالبًا ما تترك غيرها غير مرئية تمامًا. من منظور فني، هذا ليس مثاليًا، لذا سنقوم بـ”تسوية” البيانات الواردة من SignalRGB. تتضمن التسوية توزيع البيانات بالتساوي بين نقطة قصوى ونقطة دنيا لتوضيح قيمها بالنسبة لبعضها البعض بشكل أفضل. بعد تحديد القيمتين القصوى والدنيا في مصفوفة التردد، تصبح المعادلة بسيطة جدًا إجمالًا: (x - min) / (max - min)، حيث يمثل “x” العنصر الحالي.

بيانات معالجة ومُسوّاة:

بعد ذلك، سنضيف لمسة من الحيوية بترتيب أشرطة الصوت على شكل دائرة:

function soundBars (x, y){
this.x = x;
this.y = y;
this.draw = function(){
var max = Math.max(...reducedFreq)
var min = Math.min(...reducedFreq)
for(let i = 0; i < reducedFreq.length; i++){
//Save the current state of the canvas
ctx.save()
//Add the circular component to your bars. "50" here is the radius of the circle
var x = this.x + Math.cos(i) * 50
var y = this.y + Math.sin(i) * 50
var height = ((Math.abs(reducedFreq[i])) - min) / (max - min) * -40
// Determine the angle of the bar, to make it perpindicular to the circle
var rotate = Math.atan2(y - this.y, x - this.x) + Math.PI / 2;
//Translate to circle center
ctx.translate(x, y)
//Rotate the canvas
ctx.rotate(rotate)
//Translate back for drawing
ctx.translate(-x, -y)
DrawRect(x, y, 5, height, "white")
//Restore the old state of the canvas after drawing to prevent positive feedback loops
ctx.restore()
}
}
}

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

function soundBars (x, y){
this.x = x;
this.y = y;
this.rotate = 0;
this.draw = function(){
//Hue is calculated each frame, the resulting amount will be a proper hue option for hsl
var hue = engine.audio.density * 360
var max = Math.max(...reducedFreq)
var min = Math.min(...reducedFreq)
for(let i = 0; i < reducedFreq.length; i++){
ctx.save()
var x = this.x + Math.cos(i) * 50
var y = this.y + Math.sin(i) * 50
//Height slightly edited for visability
var height = ((Math.abs(reducedFreq[i])) - min) / (max - min) * -50 - 5
this.rotate = Math.atan2(y - this.y, x - this.x) + Math.PI / 2;
ctx.translate(x, y)
ctx.rotate(this.rotate)
ctx.translate(-x, -y)
//Insert the edited hue value each frame
DrawRect(x, y, 5, height, `hsl(${hue}, 100%, 50%)`)
this.rotate = 0;
ctx.restore()
}
}
}

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

function update() {
var frequency = new Int8Array(engine.audio.freq)
reducedFreq = frequency.filter((element, index) => {
return index % 4 === 0;
})
if(effects.length < 1){
effects.push(new soundBars(160, 100))
}
DrawRect(0, 0, 320, 200, "black")
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime <= 0) {
effects.splice(i, 1);
}
});
// Hue calcualtion to match the rest of the visualizer
var hue = engine.audio.density * 360
//The level has been multiplied by 10, then added to 100. This is arbitrary, but some combination of actions here will give you the result you want
DrawCircle(160, 100, 50, `hsl(${hue}, 100%, ${100 + engine.audio.level * 10}%)`)
window.requestAnimationFrame(update);
}