Click the card to play the audio clip.
First, place your mp3 audio files inside the www/audio folder
This is just a demo project, so you only need to drop b.mp3 into this directory.

Method 1‑HTML5 Audio Playback
Play audio with HTML5 Audio supported by WebView, no extra plugins required
Update main.js with the code shown below
// Vietnamese consonant dataset
const consonants = [
{char: "b", pronounce: "/ɓ/", desc: "Bilabial implosive consonant"},
{char: "c", pronounce: "/k/", desc: "Velar stop consonant"},
{char: "ch", pronounce: "/tɕ/", desc: "Palatal affricate consonant"},
{char: "d", pronounce: "/ɗ/", desc: "Alveolar implosive consonant"},
{char: "đ", pronounce: "/ɗ/", desc: "Alveolar implosive consonant"},
{char: "g", pronounce: "/ɣ/", desc: "Velar fricative consonant"},
{char: "gh", pronounce: "/ɣ/", desc: "Used before e, i"},
{char: "h", pronounce: "/h/", desc: "Glottal fricative consonant"},
{char: "k", pronounce: "/k/", desc: "Voiceless velar stop"},
{char: "kh", pronounce: "/x/", desc: "Voiceless velar fricative"},
{char: "l", pronounce: "/l/", desc: "Alveolar lateral consonant"},
{char: "m", pronounce: "/m/", desc: "Bilabial nasal consonant"},
{char: "n", pronounce: "/n/", desc: "Alveolar nasal consonant"},
{char: "ng", pronounce: "/ŋ/", desc: "Velar nasal consonant"},
{char: "ngh", pronounce: "/ŋ/", desc: "Used before e,i"},
{char: "nh", pronounce: "/ɲ/", desc: "Palatal nasal consonant"},
{char: "p", pronounce: "/p/", desc: "Unaspirated bilabial stop"},
{char: "ph", pronounce: "/f/", desc: "Labiodental fricative consonant"},
{char: "q", pronounce: "/kw/", desc: "Always paired with u"},
{char: "r", pronounce: "/r/", desc: "Alveolar trill consonant"},
{char: "s", pronounce: "/s/", desc: "Voiceless alveolar fricative"},
{char: "t", pronounce: "/t/", desc: "Voiceless alveolar stop"},
{char: "th", pronounce: "/θ/", desc: "Dental fricative consonant"},
{char: "v", pronounce: "/v/", desc: "Voiced labiodental fricative"},
{char: "x", pronounce: "/s/", desc: "Voiceless alveolar fricative"}
];
let favList = JSON.parse(localStorage.getItem("vn_consonant_fav")) || [];
let currentAudio = null; // Global audio instance, stops previous playback
const gridEl = document.getElementById("consonant-grid");
const favGridEl = document.getElementById("favorite-grid");
const modal = document.getElementById("modal");
const modalChar = document.getElementById("modal-char");
const modalPro = document.getElementById("modal-pronounce");
const modalDesc = document.getElementById("modal-desc");
const btnFav = document.getElementById("btn-fav");
const closeModal = document.getElementById("close-modal");
let currentItem = null;
// ====================== MP3 PLAY FUNCTION ======================
function playAudio(charName) {
// Stop any currently‑playing audio
if(currentAudio){
currentAudio.pause();
currentAudio.currentTime = 0;
}
// Create new audio object
currentAudio = new Audio(`audio/${charName}.mp3`);
currentAudio.volume = 1;
currentAudio.play().catch(err=>{
console.log("Failed to play file: ", charName, err);
alert(`Audio file not found: ${charName}.mp3`);
});
}
// Render consonant cards
function renderCards(){
gridEl.innerHTML = "";
consonants.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform cursor-pointer";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> {
playAudio(item.char); // Tap card to trigger mp3 playback
openModal(item);
};
gridEl.appendChild(div);
})
}
// Render favorites page
function renderFav(){
favGridEl.innerHTML = "";
if(favList.length === 0){
favGridEl.innerHTML = `<p class="col-span-3 text-center text-gray-400 py-10">No favorites added yet</p>`;
return;
}
favList.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform cursor-pointer";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> {
playAudio(item.char);
openModal(item);
};
favGridEl.appendChild(div);
})
}
// Open detail popup modal
function openModal(item){
currentItem = item;
modalChar.innerText = item.char;
modalPro.innerText = item.pronounce;
modalDesc.innerText = item.desc;
updateFavButton();
modal.classList.remove("hidden");
}
// Close popup modal
closeModal.onclick = ()=>{
modal.classList.add("hidden");
}
modal.onclick = (e)=>{
if(e.target === modal) modal.classList.add("hidden");
}
// Toggle add / remove favorite button label
function updateFavButton(){
const exist = favList.find(x=>x.char === currentItem.char);
btnFav.innerText = exist ? "Remove from favorites" : "Add to favorites";
}
btnFav.onclick = ()=>{
const idx = favList.findIndex(x=>x.char === currentItem.char);
if(idx >=0) favList.splice(idx,1);
else favList.push(currentItem);
localStorage.setItem("vn_consonant_fav", JSON.stringify(favList));
updateFavButton();
renderFav();
}
// Tab page switching logic
const tabBtns = document.querySelectorAll(".tab-btn");
const pages = document.querySelectorAll(".page");
tabBtns.forEach(btn=>{
btn.onclick = ()=>{
const targetId = btn.dataset.target;
tabBtns.forEach(b=>{
b.classList.remove("tab-active");
b.querySelector("svg").classList.add("text-gray-500");
})
btn.classList.add("tab-active");
btn.querySelector("svg").classList.remove("text-gray-500");
pages.forEach(p=>p.classList.add("hidden"));
document.getElementById(targetId).classList.remove("hidden");
}
})
document.addEventListener("DOMContentLoaded", ()=>{
renderCards();
renderFav();
})
document.addEventListener("deviceready", ()=>{
console.log("Cordova is ready");
}, false);Code language: JavaScript (javascript)
Method 2‑Plugin‑based Playback
Use cordova‑plugin‑media Cordova plugin for audio playback
Install the plugin
cordova plugin add cordova-plugin-media
Command‑line output during installation
D:\cordovademo\hello>cordova plugin add cordova-plugin-media
Installing "cordova-plugin-media" for android
Installing "cordova-plugin-file" for android
Subproject Path: CordovaLib
Subproject Path: app
The Android Persistent storage location now defaults to "Internal". Please check this plugin's README to see if your application needs any changes in its config.xml.
If this is a new application no changes are required.
If this is an update to an existing application that did not specify an "AndroidPersistentFileLocation" you may need to add:
"<preference name="AndroidPersistentFileLocation" value="Compatibility" />"
to config.xml in order for the application to find previously stored files.
Adding cordova-plugin-media to package.jsonCode language: HTML, XML (xml)
Replace the audio playback function
let mediaPlayer = null;
function playAudio(charName) {
// Stop previous audio resource
if(mediaPlayer){
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
// Important: Cordova Android path www/audio/
const src = `audio/${charName}.mp3`;
mediaPlayer = new Media(src,
()=>{
// Release resource once playback completes
mediaPlayer.release();
mediaPlayer = null;
},
(err)=>{
console.error("Audio playback error", err);
}
);
mediaPlayer.play();
}Code language: JavaScript (javascript)
Swap this snippet into your original main.js file
// Vietnamese consonant dataset
const consonants = [
{char: "b", pronounce: "/ɓ/", desc: "Bilabial implosive consonant"},
{char: "c", pronounce: "/k/", desc: "Velar stop consonant"},
{char: "ch", pronounce: "/tɕ/", desc: "Palatal affricate consonant"},
{char: "d", pronounce: "/ɗ/", desc: "Alveolar implosive consonant"},
{char: "đ", pronounce: "/ɗ/", desc: "Alveolar implosive consonant"},
{char: "g", pronounce: "/ɣ/", desc: "Velar fricative consonant"},
{char: "gh", pronounce: "/ɣ/", desc: "Used before e, i"},
{char: "h", pronounce: "/h/", desc: "Glottal fricative consonant"},
{char: "k", pronounce: "/k/", desc: "Voiceless velar stop"},
{char: "kh", pronounce: "/x/", desc: "Voiceless velar fricative"},
{char: "l", pronounce: "/l/", desc: "Alveolar lateral consonant"},
{char: "m", pronounce: "/m/", desc: "Bilabial nasal consonant"},
{char: "n", pronounce: "/n/", desc: "Alveolar nasal consonant"},
{char: "ng", pronounce: "/ŋ/", desc: "Velar nasal consonant"},
{char: "ngh", pronounce: "/ŋ/", desc: "Used before e,i"},
{char: "nh", pronounce: "/ɲ/", desc: "Palatal nasal consonant"},
{char: "p", pronounce: "/p/", desc: "Unaspirated bilabial stop"},
{char: "ph", pronounce: "/f/", desc: "Labiodental fricative consonant"},
{char: "q", pronounce: "/kw/", desc: "Always paired with u"},
{char: "r", pronounce: "/r/", desc: "Alveolar trill consonant"},
{char: "s", pronounce: "/s/", desc: "Voiceless alveolar fricative"},
{char: "t", pronounce: "/t/", desc: "Voiceless alveolar stop"},
{char: "th", pronounce: "/θ/", desc: "Dental fricative consonant"},
{char: "v", pronounce: "/v/", desc: "Voiced labiodental fricative"},
{char: "x", pronounce: "/s/", desc: "Voiceless alveolar fricative"}
];
let favList = JSON.parse(localStorage.getItem("vn_consonant_fav")) || [];
let currentAudio = null; // Global audio instance, stops previous playback
const gridEl = document.getElementById("consonant-grid");
const favGridEl = document.getElementById("favorite-grid");
const modal = document.getElementById("modal");
const modalChar = document.getElementById("modal-char");
const modalPro = document.getElementById("modal-pronounce");
const modalDesc = document.getElementById("modal-desc");
const btnFav = document.getElementById("btn-fav");
const closeModal = document.getElementById("close-modal");
let currentItem = null;
// ====================== MP3 PLAY FUNCTION ======================
let mediaPlayer = null;
function playAudio(charName) {
// Stop previous audio resource
if(mediaPlayer){
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
// Important: Cordova Android path www/audio/
const src = `audio/${charName}.mp3`;
mediaPlayer = new Media(src,
()=>{
// Release resource once playback completes
mediaPlayer.release();
mediaPlayer = null;
},
(err)=>{
console.error("Audio playback error", err);
}
);
mediaPlayer.play();
}
// Render consonant cards
function renderCards(){
gridEl.innerHTML = "";
consonants.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform cursor-pointer";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> {
playAudio(item.char); // Tap card to trigger mp3 playback
openModal(item);
};
gridEl.appendChild(div);
})
}
// Render favorites page
function renderFav(){
favGridEl.innerHTML = "";
if(favList.length === 0){
favGridEl.innerHTML = `<p class="col-span-3 text-center text-gray-400 py-10">No favorites added yet</p>`;
return;
}
favList.forEach(item=>{
const div = document.createElement("div");
div.className = "bg-cardBg rounded-lg py-5 text-center card-shadow active:scale-95 transition-transform cursor-pointer";
div.innerHTML = `<span class="text-2xl font-bold text-gray-800">${item.char}</span>`;
div.onclick = ()=> {
playAudio(item.char);
openModal(item);
};
favGridEl.appendChild(div);
})
}
// Open detail popup modal
function openModal(item){
currentItem = item;
modalChar.innerText = item.char;
modalPro.innerText = item.pronounce;
modalDesc.innerText = item.desc;
updateFavButton();
modal.classList.remove("hidden");
}
// Close popup modal
closeModal.onclick = ()=>{
modal.classList.add("hidden");
}
modal.onclick = (e)=>{
if(e.target === modal) modal.classList.add("hidden");
}
// Toggle add / remove favorite button label
function updateFavButton(){
const exist = favList.find(x=>x.char === currentItem.char);
btnFav.innerText = exist ? "Remove from favorites" : "Add to favorites";
}
btnFav.onclick = ()=>{
const idx = favList.findIndex(x=>x.char === currentItem.char);
if(idx >=0) favList.splice(idx,1);
else favList.push(currentItem);
localStorage.setItem("vn_consonant_fav", JSON.stringify(favList));
updateFavButton();
renderFav();
}
// Tab page switching logic
const tabBtns = document.querySelectorAll(".tab-btn");
const pages = document.querySelectorAll(".page");
tabBtns.forEach(btn=>{
btn.onclick = ()=>{
const targetId = btn.dataset.target;
tabBtns.forEach(b=>{
b.classList.remove("tab-active");
b.querySelector("svg").classList.add("text-gray-500");
})
btn.classList.add("tab-active");
btn.querySelector("svg").classList.remove("text-gray-500");
pages.forEach(p=>p.classList.add("hidden"));
document.getElementById(targetId).classList.remove("hidden");
}
})
document.addEventListener("DOMContentLoaded", ()=>{
renderCards();
renderFav();
})
document.addEventListener("deviceready", ()=>{
console.log("Cordova is ready");
}, false);Code language: JavaScript (javascript)
Play the audio
Previous: Vietnamese learning app UI example