Play the audio

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

Leave a Reply

Your email address will not be published. Required fields are marked *