12. Playing audio 2. Keyboard layout global and local parts
Rustam Apay
Posted on August 18, 2022
- Keyboard layout: global and local parts
- Fallback
keyboardData.en
state - Method
getKeyContent
- Method
playKey
Keyboard layout: global and local parts
Any keyboard has local specific keys, and common keys for all languages (they are without titles on the picture below):
Most common keys don’t have specific names in different languages. E.g. Escape, Tab, Caps Lock, Shift, Ctrl, Alt, Enter, Delete -- they all sound in 90% cases in English. Space, Arrows, F1-F12 usually have local names.
For now, to play audio of a key, we must put a sound file into keyboardData/langCode/
folder. It would be good, if we can use sounds from keyboardData/en/
like escape.mp3
for any language. But we will also leave the option to use local sounds for any of keys. Sounds from the folder en/
will be played only if others were not specified.
In programming such approach is called fallback
-- when something doesn't work in one way, and me make it works in another way.
Fortunately, audio.play()
returns a promise, and we can catch error if file doesn't exist, and play another file.
In our piece of keyboard the "global layout" sounds are escape
, left shift
, right shift
, tab
, f1
-f6
in the folder keyboardData/en/
.
For now, they work only for currentLang: 'en'
.
Our goal is to make en/escape.mp3
playing in ru
and ar
keyboards, if in ru/
and ar/
folders they are not specified.
In Keyboard.js
method setActiveKey add after audio.play()
:
Keyboard.js methods
...
setActiveKey(keyContent) {
const fileName = getAudioFileName(keyContent, this.shiftKey)
const audio = new Audio(
`../keyboardData/${this.currentLang}/${fileName}.mp3`
)
/* add catch after play: */
audio.play().catch(() => {
if (this.currentLang !== 'en') {
const audio = new Audio(`./keyboardData/en/${fileName}.mp3`)
audio.play()
}
})
...
}
Now audio files from the folder en/
sounds also for langs ru
and ar
. Except ShiftRight
and ShiftLeft
.
That is because of difference of keyContent
for languages. Keys escape
, tab
, f1
-f6
-- are identical for en.js
, ru.js
and ar.js
in keyboardData/
folder and getAudioFileName returns the same name for any language, and it can be played.
Keys ShiftLeft
, ShiftRight
for en
have additional field: mainName
. Compare:
keyboardData/en.js
{
code: 'ShiftLeft',
label: 'Shift',
mainName: 'left shift'
}
keyboardData/ru.js and ar.js
{
code: 'ShiftLeft',
label: 'Shift',
}
Because of this difference in keyboards data, file names for en
will be left shift
, right shift
. File names for ru
and ar
will be: shiftleft
, shiftright
(as code
).
We could add mainName
for each global
key for every language keyboard data as in en
, end audios will sound. But it is a lot of work if we'll have lots of keyboards. And it is better to improve our code once instead of continuous synchronization and data duplication.
Fallback keyboardData.en
state
Perhaps you already guessed, that we need keyContent
of keyboardData/en.js
from any language keyboard to get right file name to play audio fallback.
The problem is that for now we don't have access to different keyboardData
at the same time. When we switch languages, keyboardData
state are completely replaced by new data.
That is how we get keyContent
from keyboardData
on keydown
event.
Keyboard.js
mounted() {
this.getKeyboardData(this.currentLang)
window.addEventListener('keydown', event => {
event.preventDefault()
const { code } = event
const keyContent = this.keyboardData
.flat()
.find(elem => elem.code === code)
this.setActiveKey(keyContent)
})
...
}
We loaded keyboardData
asynchronously from the file /keyboardData/langCode.js
before. Then we get keyContent
from it by key code
.
For currentLang
we always have keyboardData
-- it is loaded to component state on mounted()
or when user clicked on langCode
in LanguageSwitcher
.
It would be good if keyboardData
for en
loaded by default at first time, will be always available as a fallback, when we need keyContent
of a global layout keyboardData/en.js
.
Let's refactor Keyboard
state, to store there all loaded keyboardData
for all langs. Find all this.keyboardData
in code, and add to it [lang]
. In template this.
isn't written, so we find there keyboardData
and add to it currentLang
.
Keyboard.js methods
async getKeyboardData(lang) {
const { default: keyboardData } = await import(
`../keyboardData/${lang}.js`
)
/* add [lang]: */>
this.keyboardData[lang] = keyboardData
}
Keyboard.js mounted
/* add [this.currentLang] */
const keyContent = this.keyboardData[this.currentLang]
.flat()
.find(elem => elem.code === code)
Keyboard.js template
<!-- add [currentLang] -->
<div
v-for="(row, index) in keyboardData[currentLang]"
:class="['row', 'row-'+(index+1)]"
></div>
Open the app. It should work as before.
Method getKeyContent
Now we have immediate access to keyboards, that we opened before, without need to load them every time.
Let's make our code more universal by creating a new method:
Keyboard.js methods:
getKeyContent(lang, code) {
return this.keyboardData[lang].flat().find(elem => elem.code === code)
}
Rewrite old code using this method.
Keyboard.js mounted:
mounted() {
this.getKeyboardData(this.currentLang)
window.addEventListener('keydown', event => {
event.preventDefault()
const { code, shiftKey } = event
/* replace :
const keyContent = this.keyboardData[this.currentLang]
.flat()
.find(elem => elem.code === code)
with : */
const keyContent = this.getKeyContent(this.currentLang, code)
this.setActiveKey(keyContent)
})
...
}
Use new method to play audio fallback:
Keyboard.js methods
setActiveKey(keyContent) {
const { code } = keyContent
const { shiftKey, currentLang } = this
// we created a new function
// because we call all this code twice in this method
const playKeyAudio = (lang, code, shiftKey) => {
const keyContent = this.getKeyContent(lang, code)
const fileName = getAudioFileName(keyContent, shiftKey)
const audio = new Audio(`../keyboardData/${lang}/${fileName}.mp3`)
return audio.play()// promise, we can catch error if file doesn't exist
}
playKeyAudio(currentLang, code, shiftKey).catch(() => { // fallback
if (this.currentLang !== 'en') {
playKeyAudio('en', code, shiftKey)
}
})
this.activeKey = keyContent
clearTimeout(this.timeout)
this.timeout = setTimeout(() => (this.activeKey = { code: '' }), 1000)
},
Check how app works. Shift
should sound with any language layout.
Method playKey
There is something wrong in our code. Playing audio happens inside setActiveKey
. But what if we want to play audio without activating key, or activate key without playing audio?
Let's create a new method playKey
and remove playing logic from setActiveKey
.
Keyboard.js methods
setActiveKey(keyContent) {
this.activeKey = keyContent
clearTimeout(this.timeout)
this.timeout = setTimeout(() => (this.activeKey = { code: '' }), 1000)
},
playKey(keyContent) {
const { code } = keyContent
const { shiftKey, currentLang } = this
const playKeyAudio = (lang, code, shiftKey) => {
const keyContent = this.getKeyContent(lang, code)
const fileName = getAudioFileName(keyContent, shiftKey)
const audio = new Audio(`../keyboardData/${lang}/${fileName}.mp3`)
return audio.play()
}
playKeyAudio(currentLang, code, shiftKey).catch(() => {
// fallback
if (this.currentLang !== 'en') {
playKeyAudio('en', code, shiftKey)
}
})
},
Find in code every setActiveKey
call, and place after it playKey
, to keep previous functionality.
Keyboard.js mounted
window.addEventListener('keydown', event => {
event.preventDefault()
const { code } = event
const keyContent = this.getKeyContent(this.currentLang, code)
this.setActiveKey(keyContent)
/* add: */
this.playKey(keyContent)
})
Also pass it to Key
component and use it there.
Keyboard.js template
<vue-key ... :playKey="playKey" />
Key.js props
props: {
...
playKey: Function,
},
Key.js methods
keyClick(keyContent) {
this.setActiveKey(keyContent)
// add:
this.playKey(keyContent)
if (keyContent.code.includes('Shift')) {
this.toggleShiftKey()
}
}
Check the app. It should work as before. But now code is more flexible, we can use it in more ways.
Posted on August 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024