केस स्टडी - रेसर की आवाज़ें

शुरुआती जानकारी

Racer एक से ज़्यादा खिलाड़ी और कई डिवाइस वाला Chrome प्रयोग है. पुराने अंदाज़ का स्लॉट कार गेम, जो अलग-अलग स्क्रीन पर खेला जाता है. फ़ोन या टैबलेट, Android या iOS पर. इसमें कोई भी शामिल हो सकता है. कोई ऐप्लिकेशन नहीं है. कोई डाउनलोड नहीं. बस मोबाइल वेब.

14islands पर अपने दोस्तों के साथ मिलकर Plan8 ने, जियोर्जियो मोरोडर के मूल कंपोज़िशन पर आधारित शानदार संगीत और आवाज़ का अनुभव तैयार किया. रेसर में रिस्पॉन्सिव इंजन की आवाज़ें और रेस के साउंड इफ़ेक्ट होते हैं. हालांकि, इससे ज़्यादा ज़रूरी है एक ऐसा डाइनैमिक म्यूज़िक मिक्स जो रेसर के जुड़ने पर खुद को कई डिवाइसों पर डिस्ट्रिब्यूट करता है. यह मल्टी-स्पीकर इंस्टॉलेशन है, जिसे स्मार्टफ़ोन से मिलकर बनाया गया है.

कई डिवाइसों को एक साथ कनेक्ट करना ऐसा कुछ था जिसके साथ हम कुछ समय से धोखा कर रहे थे. हमने संगीत से जुड़े कई प्रयोग किए थे, जिनमें आवाज़ अलग-अलग डिवाइसों पर बंट सकती थी या एक से दूसरे डिवाइस पर जाती थी. इसलिए, हम इस आइडिया को Racer पर लागू करना चाहते थे.

खास तौर पर, हम यह देखना चाहते थे कि जैसे-जैसे ज़्यादा से ज़्यादा लोग गेम में शामिल हों, वैसे-वैसे हम सभी डिवाइसों पर संगीत ट्रैक को बना पाएं. इसमें ड्रम और बेस से लेकर गिटार और सिंथेस वगैरह को शामिल करना शुरू किया जाएगा. हमने संगीत के कुछ डेमो किए और कोडिंग के बारे में गहराई से बताया. कई स्पीकर वाला इफ़ेक्ट वाकई फ़ायदेमंद था. इस समय हमें पूरी तरह सिंक करने की सुविधा नहीं मिली थी. हालांकि, जब हमें सभी डिवाइसों पर आवाज़ आने लगी, तो हमें पता चला कि हमें कुछ बेहतर करने की ज़रूरत है.

साउंड बनाना

Google Creative Lab ने साउंड और संगीत के लिए एक क्रिएटिव निर्देश तैयार किया था. हमें असली आवाज़ें रिकॉर्ड करने या साउंड लाइब्रेरी का इस्तेमाल करने के बजाय, साउंड इफ़ेक्ट बनाने के लिए ऐनालॉग सिंथेसाइज़र का इस्तेमाल करना था. हमें यह भी पता था कि ज़्यादातर मामलों में आउटपुट स्पीकर, छोटा फ़ोन या टैबलेट स्पीकर होगा. इसलिए, स्पीकर की आवाज़ को डिस्टॉर्ट होने से बचाने के लिए आवाज़ को फ़्रीक्वेंसी स्पेक्ट्रम पर सीमित करना होगा. यह एक चुनौती साबित हुई. जब हमें जियोर्जियो से पहली बार संगीत के ड्राफ़्ट मिले, तो यह हमारे लिए बहुत राहत की बात थी. इसकी वजह यह थी कि उनका कंपोज़िशन, हमारे बनाए गए गानों के साथ पूरी तरह से मेल खाता था.

इंजन की आवाज़

साउंड को प्रोग्रामिंग करने में सबसे बड़ी चुनौती, इंजन की सबसे अच्छी आवाज़ का पता लगाना और उसके काम करने का तरीका तय करना थी. यह रेस ट्रैक, F1 या नास्कर ट्रैक जैसा लगता था. इसलिए, इसमें कार तेज़ और विस्फोटक लग रही थीं. उस समय कारें बहुत छोटी थीं. इसलिए, बड़े इंजन की आवाज़, ऑडियो को विज़ुअल से कनेक्ट नहीं कर पाती थी. हालांकि, हमें मोबाइल स्पीकर पर बीफ़न की तेज़ आवाज़ वाला इंजन नहीं चल सकता था. इसलिए, हमें कुछ और पूछना था.

कुछ प्रेरणा पाने के लिए, हमने अपने दोस्त Jon Ekstrand के, मॉड्यूलर सिंथ के कलेक्शन को फ़ॉलो किया और उसमें सुधार करना शुरू कर दिया. हमने जो सुना वह हमें पसंद आया. दो ऑसिलेटर, कुछ अच्छे फ़िल्टर और एलएफ़ओ के साथ यह कुछ ऐसा लग रहा था.

पहले भी Web Audio API का इस्तेमाल करके, ऐनालॉग गियर को बड़ी सफलता के साथ फिर से तैयार किया गया था. इस वजह से, हमें उम्मीद थी कि हमने वेब ऑडियो में एक सामान्य सिंथ बनाना शुरू कर दिया था. जनरेट की गई साउंड सबसे ज़्यादा रिस्पॉन्सिव आवाज़ होगी. हालांकि, यह डिवाइस की प्रोसेसिंग पावर पर टैक्स लगेगा. विज़ुअल को आसानी से चलाने के लिए, हमें सभी संसाधनों को बचाने की कोशिश करनी पड़ी. इसलिए, हमने नई तकनीक को ऑडियो सैंपल के बजाय ऑडियो सैंपल के तौर पर इस्तेमाल किया.

इंजन की आवाज़ निकालने के लिए मॉड्यूलर सिंथ

ऐसी कई तकनीकें हैं जिनका इस्तेमाल सैंपल से इंजन की आवाज़ निकालने के लिए किया जा सकता है. कंसोल गेम में सबसे आम तरीका यह है कि अलग-अलग आरपीएम पर इंजन की कई आवाज़ों (जो ज़्यादा बेहतर होगा) की एक लेयर (लोड के साथ) मौजूद हो. इसके बाद, उनके बीच क्रॉसफ़ेड और क्रॉसपिच का इस्तेमाल किया जाए. इसके बाद, एक ही आरपीएम पर, बिना लोड के इंजन की अलग-अलग आवाज़ों की एक लेयर जोड़ें. साथ ही, उनके बीच क्रॉसफ़ेड और क्रॉसपिच भी करें. गियर शिफ़्ट करते समय इन लेयर के बीच क्रॉसफ़ेड होने की सुविधा, अगर ठीक से काम करती है, तो यह बिलकुल असली लगेगा. हालांकि, ऐसा सिर्फ़ तब होगा, जब ज़्यादा साउंड फ़ाइलें हों. क्रॉसपिचिंग बहुत चौड़ी नहीं हो सकती या उसकी आवाज़ एआई से जनरेट हुई होगी. हमें लोड होने में ज़्यादा समय नहीं देना पड़ता था. इसलिए, यह विकल्प हमारे लिए सही नहीं था. हमने हर लेयर के लिए पांच या छह साउंड फ़ाइलों को उपलब्ध कराने की कोशिश की, लेकिन आवाज़ कम नहीं थी. हमें कम फ़ाइलों के साथ रास्ता ढूंढना पड़ा.

सबसे असरदार हल यह साबित हुआ है:

  • सबसे ज़्यादा पिच / आरपीएम पर, प्रोग्राम किए गए लूप में खत्म होने वाली कार की रफ़्तार और गियर शिलिंग के साथ सिंक की गई एक साउंड फ़ाइल. Web Audio API को लूप में चलाने का काम बहुत अच्छी तरह से है, ताकि हम इसका इस्तेमाल किसी ग्लिच या पॉप-अप के बिना कर सकें.
  • आवाज़ कम होने या इंजन बंद होने वाली एक साउंड फ़ाइल.
  • आखिर में, एक साउंड फ़ाइल लूप में चलती है और एक ही आवाज़ में लगातार / प्रयोग में नहीं होती है.

ऐसा लगता है

इंजन की आवाज़ वाला ग्राफ़िक

पहले टच इवेंट / ऐक्सेलरेशन के लिए, हम शुरुआत से पहली फ़ाइल चलाएंगे. साथ ही, अगर प्लेयर थ्रॉटल को रिलीज़ करता है, तो हम उस समय की गणना करेंगे जहां से हम रिलीज़ के समय साउंड फ़ाइल में थे. इससे जब थ्रॉटल दोबारा आएगा, तो वह दूसरी (रेवेन्यू डाउन) फ़ाइल चलाए जाने के बाद, ऐक्सेलरेशन फ़ाइल में सही जगह पर चला जाएगा.

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()'>

इसलिए, सिर्फ़ तीन छोटी साउंड फ़ाइल और एक अच्छे साउंड इंजन की मदद से, हमने अगली चुनौती पर जाने का फ़ैसला किया.

सिंक किया जा रहा है

14 टापू के डेविड लिंडकविस्ट के साथ मिलकर, हमने डिवाइसों को सही तरीके से सिंक करने के लिए काम करना शुरू किया. मूल सिद्धांत आसान है. डिवाइस, सर्वर से समय और नेटवर्क के इंतज़ार के समय के फ़ैक्टर के बारे में पूछता है. इसके बाद, लोकल क्लॉक ऑफ़सेट का हिसाब लगाता है.

syncOffset = localTime - serverTime - networkLatency

इस ऑफ़सेट के साथ, कनेक्ट किए गए हर डिवाइस का समय एक जैसा है. आसान है, है ना? (सिद्धांत में फिर से.)

नेटवर्क इंतज़ार के समय की गणना की जा रही है

हम यह मान सकते हैं कि अनुरोध करने और सर्वर से जवाब पाने में लगने वाले इंतज़ार के समय को आधा माना जाता है:

networkLatency = (receivedTime - sentTime) × 0.5

इस अनुमान में समस्या यह है कि सर्वर के लिए दोतरफ़ा यात्रा हमेशा एक जैसे नहीं होती. इसका मतलब है कि अनुरोध के जवाब से ज़्यादा समय लग सकता है या इसी वजह से सर्वर के लिए दोतरफ़ा यात्रा की प्रक्रिया एक जैसी नहीं होती. नेटवर्क के इंतज़ार का समय जितना ज़्यादा होगा, इस गड़बड़ी का असर उतना ही ज़्यादा होगा. इससे, आवाज़ देर से शुरू होगी और अन्य डिवाइसों के साथ सिंक न हो पाएगी.

अच्छी बात यह है कि अगर आवाज़ थोड़ी देर से आती है, तो हमारा दिमाग इस बात पर ध्यान नहीं देता कि आवाज़ कितनी देर से आती है. कई अध्ययनों से यह पता चला है कि हमारे दिमाग को आवाज़ को अलग समझने से पहले 20 से 30 मिलीसेकंड (मि॰से॰) की देरी होती है. हालांकि, करीब 12 से 15 मि॰से॰ तक, आपको देरी से मिलने वाले सिग्नल का असर “महसूस” होने लगेगा. भले ही, आप उसे पूरी तरह “समझ” न पाए. हमने समय के लिए सिंक करने के कुछ लोकप्रिय प्रोटोकॉल और आसान विकल्पों की जांच की और उनमें से कुछ को लागू करने की कोशिश की. आखिर में, Google की इंतज़ार के समय को कम करने वाले इन्फ़्रास्ट्रक्चर की वजह से, हम कई अनुरोधों को सैंपल कर पाए और रेफ़रंस के तौर पर, सबसे कम इंतज़ार के समय वाले सैंपल का इस्तेमाल कर पाए.

लड़ाकू घड़ी का समय

इससे काम हो गया! हमारे 5 से ज़्यादा डिवाइस सही तरीके से सिंक कर रहे थे—लेकिन कुछ समय तक ही वे ऐसा कर रहे थे. कुछ मिनट तक खेलने के बाद, डिवाइसों के बीच की आवाज़ धीरे-धीरे खत्म हो जाती है. हालांकि, हमने Web Audio API के कॉन्टेक्स्ट में सबसे सटीक समय का इस्तेमाल करके, आवाज़ को शेड्यूल कर दिया है. लैग तब धीरे-धीरे इकट्ठा होता है, जब एक बार में सिर्फ़ कुछ मिलीसेकंड होते हैं और शुरुआत में इसका पता नहीं लगाया जा सकता. हालांकि, लंबे समय तक चलने के बाद, म्यूज़िकल लेयर पूरी तरह सिंक नहीं हो पातीं. नमस्ते, घड़ी का समय बदलना.

इसका समाधान था कि हर कुछ सेकंड को फिर से सिंक करें, एक नए क्लॉक ऑफ़सेट का हिसाब लगाएं, और इसे ऑडियो शेड्यूलर में आसानी से फ़ीड करें. नेटवर्क लैग की वजह से संगीत में अहम बदलावों के जोखिम को कम करने के लिए, हमने नए सिंक ऑफ़सेट का इतिहास बनाकर और औसत का हिसाब लगाकर इस बदलाव को आसान बनाने का फ़ैसला किया है.

गाना शेड्यूल करना और अरेंजमेंट स्विच करना

इंटरैक्टिव साउंड अनुभव का मतलब है कि अब आपके पास यह कंट्रोल नहीं रहेगा कि गाना कब चलेगा, क्योंकि मौजूदा स्थिति बदलने के लिए आपको उपयोगकर्ता की कार्रवाइयों पर निर्भर रहना पड़ता है. हमें यह पक्का करना था कि हम गाने की अलग-अलग सेटिंग को समय पर लागू कर सकें. इसका मतलब है कि हमारे शेड्यूलर को अगली बार स्ट्रीम करने से पहले, यह हिसाब लगाना था कि मौजूदा बार में संगीत के कितने हिस्से बचे हैं. हमारे एल्गोरिदम को कुछ ऐसा मिला:

  • Client(1) गाना शुरू करता है.
  • Client(n) पहले क्लाइंट से पूछता है कि गाना कब शुरू हुआ था.
  • Client(n) एक संदर्भ बिंदु के तौर पर, अपने वेब ऑडियो कॉन्टेक्स्ट का इस्तेमाल करके गाना शुरू करते समय, सिंक ऑफ़सेट में फ़ैक्टरिंग करते हुए और ऑडियो कॉन्टेक्स्ट बनाए जाने के बाद बीते समय को कैलकुलेट करता है.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • PlayDelta का इस्तेमाल करके, Client(n) यह बताता है कि गाना कितने समय से चल रहा है. गाने का शेड्यूलर इसका इस्तेमाल यह जानने के लिए करता है कि मौजूदा क्रम में आगे किस बार को चलाया जाना चाहिए.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

समझदारी के लिए, हमने अपनी व्यवस्था को हमेशा आठ बार लंबा और एक ही रफ़्तार (प्रति मिनट धड़कन) तक सीमित रखा.

सामने देखें

JavaScript में setTimeout या setInterval का इस्तेमाल करने के लिए, पहले शेड्यूल करना ज़रूरी है. इसकी वजह यह है कि JavaScript क्लॉक बहुत सटीक नहीं होता और शेड्यूल किए गए कॉलबैक को लेआउट, रेंडरिंग, गार्बेज कलेक्शन, और XMLHTTPRequests से आसानी से दसियों मिलीसेकंड या उससे ज़्यादा समय के हिसाब से तिरछा किया जा सकता है. हमारे मामले में, हमें सभी क्लाइंट को नेटवर्क पर एक ही इवेंट पाने में लगने वाले समय को भी ध्यान में रखना पड़ा.

ऑडियो स्प्राइट

आवाज़ों को एक फ़ाइल में मिलाकर, एचटीएमएल ऑडियो और Web Audio API, दोनों के लिए एचटीटीपी अनुरोधों को कम किया जा सकता है. यह ऑडियो ऑब्जेक्ट का इस्तेमाल करके, आवाज़ को सही तरीके से चलाने का सबसे अच्छा तरीका भी है. इसकी वजह यह है कि इसे चलाने से पहले, नए ऑडियो ऑब्जेक्ट को लोड करने की ज़रूरत नहीं होती. पहले से ही इन्हें लागू करने के कुछ अच्छे तरीके मौजूद हैं, जिन्हें हमने शुरुआत में इस्तेमाल किया. हमने iOS और Android, दोनों पर बेहतर तरीके से काम करने के लिए स्प्लिट बढ़ाए हैं. साथ ही, डिवाइसों के स्लीप मोड में जाने से जुड़ी कुछ मुश्किलों को भी हैंडल किया है.

Android पर, डिवाइस को स्लीप मोड में रखने के बाद भी ऑडियो एलिमेंट चलते रहते हैं. स्लीप मोड में, JavaScript का इस्तेमाल करने पर बैटरी की बचत होती है. साथ ही, कॉलबैक चालू करने के लिए, requestAnimationFrame, setInterval या setTimeout पर भरोसा नहीं किया जा सकता. यह एक समस्या है, क्योंकि ऑडियो स्प्राइट JavaScript का इस्तेमाल करके, यह जांच करते रहते हैं कि प्लेबैक को बंद करना है या नहीं. स्थिति को और भी खराब करने के लिए, कुछ मामलों में ऑडियो एलिमेंट का currentTime अपडेट नहीं होता. हालांकि, ऑडियो चल रहा होता है.

उस ऑडियो को लागू करने की सुविधा देखें जिसे हमने Chrome रेसर में इस्तेमाल किया है. यह एक गैर-वेब ऑडियो फ़ॉलबैक के तौर पर होता है.

ऑडियो एलिमेंट

जब हमने रेसिंग पर काम करना शुरू किया था, तब तक Android के लिए Chrome में Web Audio API की सुविधा काम नहीं करती थी. कुछ डिवाइसों के लिए एचटीएमएल ऑडियो का इस्तेमाल करने और कुछ डिवाइसों के लिए Web Audio API का इस्तेमाल करने का लॉजिक, जिसे हम कुछ दिलचस्प चुनौतियों के लिए बेहतर ऑडियो आउटपुट के साथ इस्तेमाल करना चाहते थे. अच्छी बात यह है कि अब यह सब का इतिहास हो गया है. Web Audio API को Android M28 के बीटा वर्शन में लागू किया गया है.

  • देरी/समय से जुड़ी समस्याएं. ऑडियो एलिमेंट हमेशा तब ठीक से नहीं चलता है, जब उसे चलाने के लिए कहा जाता है. JavaScript, सिंगल थ्रेड की सुविधा वाली होती है. इसलिए, ब्राउज़र व्यस्त हो सकता है. इस वजह से, वीडियो चलाने में दो सेकंड तक की देरी हो सकती है.
  • वीडियो चलाने में देरी होने का मतलब है कि वीडियो को बिना किसी रुकावट के लूप में चलाना हमेशा संभव नहीं होता. डेस्कटॉप पर, डबल बफ़रिंग का इस्तेमाल करके गैपलेस लूप पाए जा सकते हैं. हालांकि, मोबाइल डिवाइसों पर यह विकल्प नहीं है, क्योंकि:
    • ज़्यादातर मोबाइल डिवाइसों पर, एक बार में एक से ज़्यादा ऑडियो एलिमेंट नहीं चलेंगे.
    • फ़िक्स्ड वॉल्यूम. Android या iOS, दोनों में ही आपको ऑडियो ऑब्जेक्ट का वॉल्यूम कम या ज़्यादा करने की अनुमति नहीं होती.
  • पेजों को पहले से लोड करने की ज़रूरत नहीं है. मोबाइल डिवाइसों पर, ऑडियो एलिमेंट अपने सोर्स को तब तक लोड करना शुरू नहीं करेगा, जब तक touchStart हैंडलर में वीडियो चलाने की प्रोसेस शुरू नहीं होती.
  • समस्याओं के बारे में जानकारी पाना. जब तक आपका सर्वर एचटीटीपी बाइट-रेंज की सुविधा नहीं देता है, तब तक duration की सेटिंग या currentTime सेट नहीं किया जा सकेगा. अगर आप भी हमारी तरह ऑडियो स्प्राइट बना रहे हैं, तो इसे देखते रहें.
  • MP3 पर बेसिक पुष्टि नहीं हो पाती. कुछ डिवाइस, बेसिक पुष्टि की मदद से सुरक्षित की गई MP3 फ़ाइलें लोड नहीं कर पाते, चाहे आप किसी भी ब्राउज़र का इस्तेमाल कर रहे हों.

मीटिंग में सामने आए नतीजे

वेब के लिए आवाज़ को रोकने के सबसे अच्छे विकल्प के रूप में म्यूट बटन दबाने से लेकर अब तक हमने बहुत आगे बढ़ चुके हैं, लेकिन यह तो बस शुरुआत है और वेब ऑडियो जोर-जोर से चलने वाला है. हालांकि, हमने अब तक यह नहीं बताया है कि एक से ज़्यादा डिवाइसों को सिंक करने के बाद क्या किया जा सकता है. हमारे पास फ़ोन और टैबलेट में, सिग्नल प्रोसेसिंग और इफ़ेक्ट (जैसे कि रीवर्ब) के बारे में जानने के लिए प्रोसेसिंग की क्षमता नहीं थी. हालांकि, डिवाइस की परफ़ॉर्मेंस बढ़ने के साथ-साथ वेब आधारित गेम में भी इन सुविधाओं का फ़ायदा लिया जा सकता है. यह समय बहुत अच्छा है, क्योंकि साउंड की संभावनाओं को बढ़ाने के लिए लगातार काम किए जा रहे हैं.