ข้ามไปยังเนื้อหา

ออดิโอวิชวลไลเซอร์

ในส่วนนี้ เราจะเจาะลึกคุณสมบัติด้านเสียงบางส่วนของ API และแสดงพื้นฐานการสร้างออดิโอวิชวลไลเซอร์ให้คุณดู วิชวลไลเซอร์ส่วนใหญ่สร้างขึ้นจากหลักการพื้นฐานชุดเดียวกัน ดังนั้นเพียงฝึกฝนเล็กน้อยก็สามารถสร้างผลลัพธ์ที่สนุกมาก ๆ ได้

คุณสามารถเข้าถึงข้อมูลเสียงที่ SignalRGB มอบให้ได้ผ่านคุณสมบัติไม่กี่ตัวในโค้ดของคุณ:

  • engine.audio.level - คืนค่าตัวเลขระหว่าง -100 ถึง 0 ซึ่งแสดงถึงความดังโดยรวมของแทร็ก 0 คือดัง ส่วน -100 คือเบามาก
  • engine.audio.density - คืนค่าตัวเลขระหว่าง 0 ถึง 1 ซึ่งแสดงถึงความหยาบของโทนเสียง โดยเสียงทดสอบ (test tone) จะคืนค่า 0 และไวต์นอยส์จะคืนค่า 1
  • engine.audio.freq - คืนค่าอาร์เรย์ 200 สมาชิกที่มีข้อมูลความถี่ของแทร็ก

คุณสมบัติแต่ละตัวต้องผ่านการนอร์มัลไลซ์หรือการปรับแต่งในระดับที่แตกต่างกันก่อนจะนำไปใช้งานได้อย่างเหมาะสม ซึ่งจะอธิบายในอีกสักครู่

มาเริ่มต้นด้วยแอนิเมชันความถี่พื้นฐานกัน

ความถี่แสดงถึงระดับเสียง (pitch) ที่เราได้ยิน และเป็นคุณสมบัติที่สำคัญที่สุดสำหรับออดิโอวิชวลไลเซอร์ สิ่งที่เราทำในที่นี้คือการนำคลื่นความถี่ 200 ส่วนในแต่ละเฟรมมาแปลงเป็นภาพ ขั้นตอนพื้นฐานมีดังนี้:

  • สร้างอาร์เรย์และเติมข้อมูลความถี่ลงไป
  • แก้ไขอาร์เรย์นี้ให้เหมาะกับความต้องการของคุณ (filter, map, reduce ฯลฯ)
  • เขียนคลาส “แท่งเสียง” (sound bar) เพื่อแทนสมาชิกแต่ละตัว
  • เชื่อมข้อมูลเข้ากับคลาสแท่งเสียงในทุกเฟรม

สิ่งสำคัญเกี่ยวกับความถี่คือ เราต้องปรับข้อมูลดิบสองอย่าง บางครั้งสมาชิกอาจมีค่าติดลบ ซึ่งทำให้ภาพดูสะดุดตา นอกจากนี้ความสูงของสมาชิกในตัวอย่างนี้ก็ยังไม่ถูกต้อง เนื่องจากค่าบวกในตัวเลือก “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()
}
}
}

ความหนาแน่น (density) ใช้งานง่าย ค่าที่คืนกลับมาจะเป็นตัวเลขระหว่าง 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);
}