กรณีศึกษา - The Sounds of Racer

เกริ่นนำ

Racer เป็นการทดสอบ Chrome แบบหลายอุปกรณ์แบบหลายอุปกรณ์ เกมสล็อตรถสล็อตสไตล์ย้อนยุคที่เล่นบนหน้าจอต่างๆ ในโทรศัพท์หรือแท็บเล็ต Android หรือ iOS ทุกคนสามารถเข้าร่วมได้ ไม่มีแอป ไม่ต้องดาวน์โหลด เฉพาะเว็บบนอุปกรณ์เคลื่อนที่

ช่อง Plan8 กับเพื่อนๆ ที่ 14islands สร้างประสบการณ์การฟังเพลงและฟังเสียงที่มีชีวิตชีวาโดยใช้การเรียบเรียงเพลงต้นฉบับของ Giorgio Moroder เวอร์ชันนักแข่งมาพร้อมเสียงเครื่องยนต์ที่ตอบสนองได้ เอฟเฟกต์เสียงสำหรับการแข่งรถ แต่ที่สำคัญกว่านั้นคือมิกซ์เพลงแบบไดนามิกที่กระจายตัวเองผ่านอุปกรณ์ต่างๆ เมื่อนักแข่งเข้าร่วม เป็นการติดตั้งลำโพงหลายตัวซึ่งประกอบด้วยสมาร์ทโฟน

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

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

การสร้างเสียง

Google Creative Lab ได้กำหนดทิศทางการสร้างสรรค์สำหรับเสียงและดนตรี เราต้องการใช้เครื่องสังเคราะห์เสียงแบบแอนะล็อกเพื่อสร้างเอฟเฟกต์เสียงมากกว่าการบันทึกเสียงจริงหรือเปลี่ยนไปใช้คลังเสียง เรายังรู้ด้วยว่าในกรณีส่วนใหญ่ลำโพงเอาต์พุตจะเป็นลำโพงโทรศัพท์หรือแท็บเล็ตขนาดเล็ก ดังนั้นเสียงจึงต้องจำกัดสเปกตรัมของเสียงเพื่อไม่ให้ลำโพงบิดเบี้ยว ซึ่งพิสูจน์ให้เห็นแล้วว่าท้าทายมาก เมื่อได้รับร่างเพลงครั้งแรกจาก Giorgio เราก็ตื่นเต้นมากเพราะบทประพันธ์เพลงของเขาทำงานกับเสียงที่เราสร้างขึ้นได้อย่างดีเยี่ยม

เสียงเครื่องยนต์

ความท้าทายที่ยากที่สุดในการเขียนโปรแกรมเสียงคือการหาเสียงเครื่องยนต์ที่ดีที่สุดและพัฒนาลักษณะการทำงาน สนามแข่งคล้ายกับสนาม F1 หรือ Nascar รถจึงต้องเร็วและระเบิดได้ ขณะเดียวกัน รถยังมีขนาดเล็กมาก ดังนั้นเสียงเครื่องยนต์ขนาดใหญ่จึงไม่ได้เชื่อมโยงเสียงเข้ากับภาพ ถึงตอนนี้ เราก็คงไม่มีเสียงเครื่องเสียงกึกก้องในลำโพงมือถือ เลยต้องไปคิดหาอย่างอื่น

เพื่อหาแรงบันดาลใจ เราได้รวบรวมคอลเล็กชันการสังเคราะห์แบบโมดูลาร์ของ Jon Ekstrand เพื่อนของเราและมาเริ่มสร้างความประหลาดใจ เราชอบสิ่งที่ได้ยิน เสียงที่ได้จะดูเหมือนมีออสซิลเลเตอร์ 2 ตัว ฟิลเตอร์สวยๆ และ LFO

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

การสังเคราะห์แบบโมดูลาร์สำหรับเสียงเครื่องยนต์

มีเทคนิคหลายอย่างที่สามารถนำมาใช้เพื่อทำให้เสียงเครื่องยนต์ดังจากตัวอย่าง แนวทางที่นิยมใช้กันมากที่สุดสำหรับเกมคอนโซลคือการใส่เสียงหลายๆ ชั้น (ยิ่งดีกว่า) ของเครื่องเสียงที่ RPM ต่างกัน (มีภาระงาน) แล้วค่อยๆ ครอสเฟดและครอสฟิตระหว่างเสียงเหล่านั้น จากนั้นเพิ่มชั้นเสียงต่างๆ ของเครื่องยนต์ที่จะเปลี่ยน (ไม่มีการโหลด) ที่ RPM เดียวกัน และเชื่อมต่อครอสเฟดและครอสพิตช์ระหว่างเพลง การครอสเฟดระหว่างเลเยอร์เหล่านั้นเมื่อเปลี่ยนเฟือง ถ้าทำอย่างถูกต้อง จะฟังดูสมจริงมาก แต่ก็ต่อเมื่อคุณมีไฟล์เสียงจำนวนมาก ภาพครอสพิตช์ต้องไม่กว้างเกินไป ไม่เช่นนั้นเสียงจะฟังดูสังเคราะห์มาก เนื่องจากเราต้องหลีกเลี่ยงการโหลดเป็นเวลานาน ตัวเลือกนี้จึงไม่ดีสำหรับเรา เราลองทดสอบกับไฟล์เสียง 5 หรือ 6 ไฟล์สำหรับแต่ละเลเยอร์ แต่เสียงที่ได้นั้นถือว่าน่าผิดหวัง เราต้องหาวิธีใช้ไฟล์ให้น้อยลง

โซลูชันที่มีประสิทธิภาพที่สุดได้รับการพิสูจน์แล้วว่าเป็นโซลูชันต่อไปนี้

  • ไฟล์เสียง 1 ไฟล์ที่มีการเร่งความเร็วและการเปลี่ยนเกียร์ซิงค์กับการเร่งภาพของรถยนต์ซึ่งปิดท้ายด้วยลูปที่ตั้งโปรแกรมไว้ที่ระดับเสียง / RPM สูงสุด Web Audio API ทำงานวนซ้ำอย่างแม่นยำได้อย่างดีเยี่ยมโดยไม่มีข้อบกพร่องหรือข้อผิดพลาด
  • ไฟล์เสียง 1 ไฟล์ที่มีการลดความเร็ว / เครื่องยนต์กำลังหมุนลง
  • และสุดท้าย ไฟล์เสียง 1 ไฟล์กำลังเล่นเสียงนิ่ง / ไม่มีความเคลื่อนไหวแบบวนซ้ำ

มีลักษณะแบบนี้

กราฟิกเสียงเครื่องยนต์

สำหรับเหตุการณ์การแตะครั้งแรก / การเร่งความเร็ว เราจะเล่นไฟล์แรกตั้งแต่ต้น และหากโปรแกรมเล่นปล่อยคันเร่ง เราจะคำนวณเวลาจากตำแหน่งที่เราอยู่ในไฟล์เสียงตอนปล่อย เพื่อที่ว่าเมื่อควบคุมความเร็วอีกครั้ง เสียงจะกระโดดไปยังตำแหน่งที่ถูกต้องในไฟล์การเร่งหลังจากที่เล่นไฟล์ที่สอง (เลื่อนลง)

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

ทดลองได้เลย

สตาร์ทเครื่องแล้วกดปุ่ม "คันเร่ง"

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

ดังนั้น ด้วยไฟล์เสียงเล็กๆ เพียง 3 ไฟล์และเครื่องเสียงที่ดี เราจึงตัดสินใจต่อไปยังความท้าทายถัดไป

การซิงค์

โดยเราด้วยความร่วมมือจาก David Lindkvisor จาก 14 เกาะ เราจึงเริ่มเจาะลึกเกี่ยวกับการทำให้อุปกรณ์ต่างๆ เล่นร่วมกันได้สมบูรณ์แบบ ทฤษฎีพื้นฐานง่ายๆ อุปกรณ์จะขอเวลาจากเซิร์ฟเวอร์ รวมถึงพิจารณาเวลาในการตอบสนองของเครือข่าย จากนั้นจะคำนวณออฟเซ็ตนาฬิกาท้องถิ่น

syncOffset = localTime - serverTime - networkLatency

การปรับค่าชดเชยนี้แต่ละอุปกรณ์ที่เชื่อมต่อจะใช้แนวคิดของเวลาเดียวกัน ง่ายใช่ไหม (ทฤษฎีก็เช่นกัน)

กำลังคำนวณเวลาในการตอบสนองของเครือข่าย

เราอาจคิดว่าเวลาในการตอบสนองคือครึ่งหนึ่งของเวลาที่ใช้ในการขอและได้รับการตอบกลับจากเซิร์ฟเวอร์

networkLatency = (receivedTime - sentTime) × 0.5

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

โชคดีที่สมองของเราไม่สามารถสังเกตได้หากมีเสียงที่ล่าช้าเล็กน้อย การศึกษาแสดงให้เห็นว่าสมองของเราต้องใช้เวลาถึง 20 ถึง 30 มิลลิวินาที (ms) ก่อนที่สมองของเราจะรับรู้เสียงได้ อย่างไรก็ตาม ประมาณ 12 ถึง 15 มิลลิวินาที คุณจะเริ่ม "รู้สึก" ถึงผลกระทบของสัญญาณที่ล่าช้าแม้ว่าจะ "รับรู้" ไม่ได้ก็ตาม เราได้ตรวจสอบโปรโตคอลการซิงค์เวลาที่สร้างขึ้น 2 รายการ ทางเลือกอื่นที่ง่ายกว่า และได้ลองใช้โปรโตคอลเหล่านั้นในทางปฏิบัติ ท้ายที่สุดแล้ว ต้องขอบคุณโครงสร้างพื้นฐานที่มีเวลาในการตอบสนองต่ำของ Google ทำให้เราสามารถสุ่มตัวอย่างคำขอจำนวนมากและใช้ตัวอย่างที่มีเวลาในการตอบสนองต่ำสุดเป็นข้อมูลอ้างอิงได้

ต่อสู้เลื่อนนาฬิกา

ใช้งานได้แล้ว! เรามีอุปกรณ์มากกว่า 5 เครื่องที่ทำงานกันอย่างต่อเนื่องอย่างสมบูรณ์แบบ แต่ใช้ไปได้ระยะหนึ่งเท่านั้น หลังจากเล่นไป 2-3 นาที อุปกรณ์ก็จะหลุดออกจากกันแม้ว่าเราจะกำหนดเวลาเสียงโดยใช้เวลาตามบริบทของ Web Audio API ที่มีความแม่นยำสูงก็ตาม เวลาหน่วงจะสะสมอย่างช้าๆ โดยเวลาเพียงครั้งละ 2-3 มิลลิวินาทีและตรวจจับไม่ได้ในตอนแรก แต่ส่งผลให้เลเยอร์เพลงไม่ตรงกันทั้งหมดหลังจากเล่นไปเป็นเวลานาน สวัสดี นาฬิกาเลื่อนเวลา

วิธีแก้ไขคือการซิงค์ใหม่ทุกๆ 2-3 วินาที คำนวณการชดเชยเวลานาฬิกาใหม่ และป้อนข้อมูลลงในเครื่องจัดตารางเวลาเสียงอย่างราบรื่น เพื่อลดความเสี่ยงของการเปลี่ยนแปลงที่สำคัญของเพลงเนื่องจากความล่าช้าของเครือข่าย เราได้ตัดสินใจที่จะทำให้การเปลี่ยนแปลงเป็นไปอย่างราบรื่นด้วยการเก็บประวัติออฟเซ็ตการซิงค์ล่าสุดและคำนวณค่าเฉลี่ย

การกำหนดเวลาเพลงและสลับการจัดเรียงเพลง

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

  • Client(1) เริ่มเล่นเพลง
  • Client(n) ถามลูกค้ารายแรกว่าเพลงเริ่มเล่นเมื่อใด
  • Client(n) คำนวณจุดอ้างอิงไปยังเวลาที่เพลงเริ่มต้นโดยใช้บริบท Web Audio โดยนำ SyncOffset มาพิจารณา รวมถึงเวลาที่ผ่านไปนับตั้งแต่การสร้างบริบทเสียง
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) คำนวณระยะเวลาที่เพลงเล่นโดยใช้ playDelta เครื่องจัดตารางเวลาเพลงใช้ข้อมูลนี้เพื่อให้ทราบว่าแถบใดในการจัดเรียงปัจจุบันควรเล่นเป็นรายการถัดไป
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

เพื่อความสะอาด เราจำกัดการจัดการของเราให้ยาว 8 ขีดและมีจังหวะความเร็วเท่ากันเสมอ (ครั้งต่อนาที)

มองไปข้างหน้า

คุณควรตั้งเวลาล่วงหน้าเมื่อใช้ setTimeout หรือ setInterval ใน JavaScript เสมอ เนื่องจากนาฬิกา JavaScript ไม่ได้แม่นยำมากนัก และการเรียกกลับที่กำหนดเวลาไว้อาจมีการบิดเบือนได้โดยง่ายเป็นเวลาหลายสิบมิลลิวินาทีขึ้นไปตามการออกแบบ การแสดงผล การรวบรวมขยะ และ XMLHTTPRequest ในกรณีของเรา เรายังต้องคำนึงถึงระยะเวลาที่ลูกค้าทุกรายได้รับเหตุการณ์เดียวกันผ่านทางเครือข่ายด้วย

เสียงสไปรท์

การรวมเสียงไว้ในไฟล์เดียวเป็นวิธีที่ยอดเยี่ยมในการลดคำขอ HTTP ทั้งสำหรับเสียง HTML และ Web Audio API นอกจากนี้ยังเป็นวิธีที่ดีที่สุดในการเล่นเสียงแบบตอบสนองตามอุปกรณ์โดยใช้ออบเจ็กต์เสียง เนื่องจากไม่จำเป็นต้องโหลดออบเจ็กต์เสียงใหม่ก่อนที่จะเล่น เรามีการติดตั้งใช้งานที่ดีบางอย่างที่เราใช้เป็นจุดเริ่มต้นอยู่แล้ว เราได้ขยายการให้บริการนี้ให้ทำงานได้อย่างมีประสิทธิภาพทั้งบน iOS และ Android รวมถึงการจัดการกรณีแปลกๆ เมื่ออุปกรณ์หลับไป

ใน Android องค์ประกอบเสียงจะยังเล่นต่อไปแม้ว่าคุณจะให้อุปกรณ์เข้าสู่โหมดสลีปก็ตาม ในโหมดสลีป ระบบจะจำกัดการเรียกใช้ JavaScript เพื่อประหยัดแบตเตอรี่ และคุณจะใช้ requestAnimationFrame, setInterval หรือ setTimeout เพื่อเริ่มการทำงานของโค้ดเรียกกลับไม่ได้ ซึ่งเป็นปัญหาเนื่องจากสไปรท์เสียงจะอาศัย JavaScript ในการตรวจสอบว่าควรหยุดการเล่นหรือไม่ แต่ในบางกรณีที่แย่ลง currentTime ขององค์ประกอบเสียงจะไม่อัปเดตแม้ว่าเสียงจะยังเล่นอยู่

ดูการใช้งาน AudioSprite ที่เราใช้ใน Chrome Racer เป็นโฆษณาสำรองที่ไม่ใช่ Web Audio

องค์ประกอบเสียง

เมื่อเราเริ่มพัฒนา Racer ขึ้นมา Chrome สำหรับ Android ยังไม่รองรับ Web Audio API ตรรกะของการใช้เสียง HTML สำหรับอุปกรณ์บางอย่างหรือ Web Audio API สำหรับอุปกรณ์อื่นๆ ร่วมกับเอาต์พุตเสียงขั้นสูงที่เราต้องการทำให้สำเร็จเพื่อความท้าทายที่น่าสนใจบางอย่าง โชคดีที่ตอนนี้มีประวัติทั้งหมด Web Audio API ใช้งานใน Android M28 รุ่นเบต้า

  • ปัญหาความล่าช้า/ระยะเวลา องค์ประกอบเสียงจะไม่เล่นทุกครั้งเมื่อคุณบอกให้เล่น เนื่องจาก JavaScript เป็นเทรดเดี่ยว เบราว์เซอร์อาจไม่ว่าง ทำให้การเล่นล่าช้าถึง 2 วินาที
  • ความล่าช้าในการเล่นอาจทำให้วนซ้ำแบบลื่นไหลไม่ได้เสมอไป ในเดสก์ท็อป คุณสามารถใช้การบัฟเฟอร์ 2 ครั้งเพื่อให้วนซ้ำได้แบบไม่ขาดตอน แต่ตัวเลือกนี้ใช้ไม่ได้กับอุปกรณ์เคลื่อนที่ เนื่องด้วยสาเหตุต่อไปนี้
    • อุปกรณ์เคลื่อนที่ส่วนใหญ่จะไม่เล่นองค์ประกอบเสียงมากกว่า 1 รายการต่อครั้ง
    • ระดับเสียงคงที่ ทั้ง Android และ iOS ไม่อนุญาตให้คุณเปลี่ยนระดับเสียงของออบเจ็กต์เสียง
  • ไม่ต้องโหลดล่วงหน้า ในอุปกรณ์เคลื่อนที่ องค์ประกอบเสียงจะไม่เริ่มโหลดแหล่งที่มา เว้นแต่จะเริ่มต้นการเล่นในเครื่องจัดการ touchStart
  • ค้นหาปัญหา การรับ duration หรือการตั้งค่า currentTime จะไม่สำเร็จ เว้นแต่เซิร์ฟเวอร์ของคุณจะรองรับช่วงไบต์ของ HTTP ระวังไว้ให้ดี ถ้าคุณกำลังสร้างสไปรท์เสียงเหมือนที่เราทำอยู่
  • การตรวจสอบสิทธิ์พื้นฐานสำหรับ MP3 ล้มเหลว อุปกรณ์บางเครื่องโหลดไฟล์ MP3 ที่มีการป้องกันโดยการตรวจสอบสิทธิ์พื้นฐานไม่ได้ ไม่ว่าคุณจะใช้เบราว์เซอร์ใดก็ตาม

บทสรุป

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