12. Playing audio 2. Keyboard layout global and local parts

apayrus

Rustam Apay

Posted on August 18, 2022

12. Playing audio 2. Keyboard layout global and local parts

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):

Image description

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()
        }
    })
    ...
}
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

keyboardData/ru.js and ar.js

{
    code: 'ShiftLeft',
    label: 'Shift',
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Keyboard.js mounted

/* add [this.currentLang] */
const keyContent = this.keyboardData[this.currentLang]
    .flat()
    .find(elem => elem.code === code)
Enter fullscreen mode Exit fullscreen mode

Keyboard.js template

<!-- add [currentLang] -->
<div
    v-for="(row, index) in keyboardData[currentLang]"
    :class="['row', 'row-'+(index+1)]"
></div>
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
        })
        ...
        }
Enter fullscreen mode Exit fullscreen mode

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)
        },
Enter fullscreen mode Exit fullscreen mode

Check how app works. Shift should sound with any language layout.

Diffs in code 12.1

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)
        }
    })
},
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

Also pass it to Key component and use it there.

Keyboard.js template

<vue-key ... :playKey="playKey" />
Enter fullscreen mode Exit fullscreen mode

Key.js props

props: {
    ...
    playKey: Function,
    },
Enter fullscreen mode Exit fullscreen mode

Key.js methods

keyClick(keyContent) {
            this.setActiveKey(keyContent)
            // add:
            this.playKey(keyContent)

            if (keyContent.code.includes('Shift')) {
                this.toggleShiftKey()
            }
        }
Enter fullscreen mode Exit fullscreen mode

Check the app. It should work as before. But now code is more flexible, we can use it in more ways.

Diffs in code 12.2

Entire code after the chapter 12

💖 💪 🙅 🚩
apayrus
Rustam Apay

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