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

บทนำ

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

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

การเชื่อมต่ออุปกรณ์หลายเครื่องเข้าด้วยกันเป็นสิ่งที่เราพยายามทำมาระยะหนึ่งแล้ว เราได้ทำการทดลองกับเพลงที่เสียงจะแยกออกเป็น 2 ฝั่งในอุปกรณ์ต่างๆ หรือกระโดดไปมาระหว่างอุปกรณ์ต่างๆ เราจึงอยากนำแนวคิดเหล่านั้นไปใช้กับฟีเจอร์ 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 ไฟล์ที่เล่นเสียง "หยุดชั่วคราว / ไม่มีการใช้งาน" แบบวนซ้ำ

มีลักษณะดังนี้

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

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

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 Lindkvist จาก 14islands ได้เริ่มศึกษาเพิ่มเติมเกี่ยวกับการทำให้อุปกรณ์เล่นอย่างสอดคล้องกัน หลักการพื้นฐานนั้นง่ายมาก อุปกรณ์จะขอเวลาจากเซิร์ฟเวอร์ โดยพิจารณาเวลาในการตอบสนองของเครือข่าย จากนั้นคำนวณการเลื่อนเวลาของนาฬิกาท้องถิ่น

syncOffset = localTime - serverTime - networkLatency

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

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

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

networkLatency = (receivedTime - sentTime) × 0.5

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

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

การแก้ไขความคลาดเคลื่อนของนาฬิกา

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

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

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

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

  • Client(1) เริ่มเล่นเพลง
  • Client(n) ถามไคลเอ็นต์รายแรกว่าเพลงเริ่มเล่นเมื่อใด
  • Client(n) จะคำนวณจุดอ้างอิงสำหรับเวลาที่เริ่มเล่นเพลงโดยใช้บริบทเสียงบนเว็บ โดยพิจารณาจาก 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 Audio และ Web Audio API และยังเป็นวิธีที่ดีที่สุดในการเล่นเสียงแบบตอบสนองโดยใช้ออบเจ็กต์เสียง เนื่องจากไม่ต้องโหลดออบเจ็กต์เสียงใหม่ก่อนเล่น มีการใช้งานที่ดีอยู่แล้วซึ่งเราใช้เป็นจุดเริ่มต้น เราได้ขยายสไปรต์ให้ทำงานได้อย่างน่าเชื่อถือทั้งใน iOS และ Android รวมถึงจัดการกับบางกรณีที่อุปกรณ์เข้าสู่โหมดสลีป

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

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

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

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

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

สรุป

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