12. Playing audio 2. Keyboard layout global and local parts
Tags: webdev, javascript, beginners, tutorial
Published: 2022-08-18 05:43:51

---
published: true
title: 12. Playing audio 2. Keyboard layout global and local parts
tags: webdev, javascript, beginners, tutorial
cover_image: https://res.cloudinary.com/practicaldev/image/fetch/s--2Ycgb9E_--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t7manuew9dwi5jlrf8p5.png
series: keyboard-learning-app
published_at: '2022-08-18T05:43:51Z'
---

- [Keyboard layout: global and local parts](#keyboard-layout-global-and-local-parts)
- [Fallback `keyboardData.en` state](#fallback-keyboarddataen-state)
- [Method `getKeyContent`](#method-getkeycontent)
- [Method `playKey`](#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):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egt4uwrarcrwqo18lb1a.png)

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

```js
...
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

```js
{
	code: 'ShiftLeft',
	label: 'Shift',
	mainName: 'left shift'
}
```

keyboardData/ru.js and ar.js

```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

```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

```js
async getKeyboardData(lang) {
	const { default: keyboardData } = await import(
		`../keyboardData/${lang}.js`
	)
	/* add [lang]: */>
	this.keyboardData[lang] = keyboardData
}
```

Keyboard.js mounted

```js
/* add [this.currentLang] */
const keyContent = this.keyboardData[this.currentLang]
	.flat()
	.find(elem => elem.code === code)
```

Keyboard.js template

```html
<!-- 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:

```js
getKeyContent(lang, code) {
	return this.keyboardData[lang].flat().find(elem => elem.code === code)
}
```

Rewrite old code using this method.

Keyboard.js mounted:

```js
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

```js
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.

[Diffs in code 12.1](https://github.com/ApayRus/keyboard/commit/355cd8e2a6ad672b19dfbaf260b416b68887170b)

## 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

```js
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

```js
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

```html
<vue-key ... :playKey="playKey" />
```

Key.js props

```js
props: {
	...
	playKey: Function,
	},
```

Key.js methods

```js
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.

[Diffs in code 12.2](https://github.com/ApayRus/keyboard/commit/51b3c513babc3fcb5e2f7b23da71e27bdc096443)

[Entire code after the chapter 12](https://github.com/ApayRus/keyboard/tree/12.-Play-audio.-Global-and-local-keyboard-layouts-)