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

परिचय

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

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

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

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

आवाज़ें बनाना

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

इंजन की आवाज़

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

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

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

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

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

सबसे असरदार तरीका यह साबित हुआ:

  • कार की रफ़्तार बढ़ाने और गियर बदलने की आवाज़ वाली एक साउंड फ़ाइल, जो कार की रफ़्तार बढ़ाने के विज़ुअल के साथ सिंक हो. यह फ़ाइल, सबसे ज़्यादा पिच / आरपीएम पर प्रोग्राम किए गए लूप में खत्म होनी चाहिए. 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()'>

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

सिंक करने की सुविधा

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

syncOffset = localTime - serverTime - networkLatency

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

नेटवर्क से कनेक्ट होने में लगने वाले समय का हिसाब लगाना

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

networkLatency = (receivedTime - sentTime) × 0.5

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

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

क्लॉक ड्रिफ़्ट को रोकना

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

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

गाने को शेड्यूल करना और उसके क्रम में बदलाव करना

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

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

हमने अपने अरेंजमेंट को हमेशा आठ बार तक सीमित रखा है. साथ ही, इनमें एक ही टेम्पो (हर मिनट में बीट) का इस्तेमाल किया है.

आगे की जानकारी

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

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

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

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

AudioSprite को लागू करने का तरीका देखें. हमने Chrome Racer में, वेब ऑडियो के फ़ॉलबैक के तौर पर इसका इस्तेमाल किया है.

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

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

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

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

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