{"id":419,"date":"2025-10-03T15:38:58","date_gmt":"2025-10-03T06:38:58","guid":{"rendered":"https:\/\/nichimofoods.com\/en\/?page_id=419"},"modified":"2025-10-16T13:02:53","modified_gmt":"2025-10-16T04:02:53","slug":"karaoke","status":"publish","type":"page","link":"https:\/\/nichimofoods.com\/en\/karaoke\/","title":{"rendered":"Unleash Your Inner Star with Karaoke Magic"},"content":{"rendered":"<div id=\"karaoke\">\r\n  <div id=\"song-list\">\r\n    <!-- No results message -->\r\n    <div id=\"no-results\">\r\n      No matching songs found.\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div id=\"search-container\">\r\n    <div id=\"search-container-wrapper\">\r\n      <!-- Music Icon -->\r\n      <span id=\"music-icon\">&#127925;<\/span>\r\n\r\n      <!-- Search Input -->\r\n      <input type=\"text\" id=\"song-search\" placeholder=\"Search for a song...\"\/>\r\n\r\n      <!-- Color Pallete-->\r\n      <button id=\"palette-btn\" title=\"Change background color\">&#x1F3A8;<\/button>\r\n      <input type=\"color\" id=\"bg-color-picker\" style=\"display:none;\">\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <ul class=\"karaoke__lyrics\">\r\n  <\/ul>\r\n\r\n  <div class=\"karaoke__media-controls\">\r\n    <!-- Linear progress bar -->\r\n    <div class=\"karaoke__progress-line\">\r\n      <div class=\"karaoke__progress-fill\"><\/div>\r\n    <\/div>\r\n    <button id=\"karaoke-button\" class=\"karaoke__control-button\" style=\"display: none;\">&#038;#x1f3b2 RANDOM<\/button>\r\n  <\/div>\r\n\r\n  <audio class=\"karaoke__audio\" preload=\"auto\"><\/audio>\r\n\r\n  <div id=\"loader\" class=\"loader\" style=\"display: none;\"><\/div>\r\n<\/div>\r\n\r\n<script>\r\ndocument.addEventListener(\"DOMContentLoaded\", function () {\r\n  let songsList = [];\r\n\r\n  function showLoader() {\r\n    document.getElementById(\"loader\").style.display = \"block\";\r\n  }\r\n\r\n  function hideLoader() {\r\n    document.getElementById(\"loader\").style.display = \"none\";\r\n  }\r\n\r\n  class EventBus {\r\n    constructor() {\r\n      this.events = {};\r\n    }\r\n    subscribe(event, callback) {\r\n      if (!this.events[event]) this.events[event] = [];\r\n      this.events[event].push(callback);\r\n    }\r\n    publish(event, data) {\r\n      if (!this.events[event]) return;\r\n      this.events[event].forEach(cb => cb(data));\r\n    }\r\n  }\r\n\r\n  const bus = new EventBus();\r\n\r\n  class AudioPlayer {\r\n    constructor(audioElem) {\r\n      this.audioElem = audioElem;\r\n      bus.subscribe(\"play\", () => this.audioElem.play());\r\n      bus.subscribe(\"pause\", () => this.audioElem.pause());\r\n      bus.subscribe(\"loadSong\", url => this.loadSong(url));\r\n\r\n      \/\/ Cleanup handlers\r\n      bus.subscribe(\"unloadSong\", () => {\r\n        if (currentLyricsComponent) {\r\n          currentLyricsComponent.unsubscribe();\r\n          currentLyricsComponent = null;\r\n        }\r\n        if (currentKaraokeButton) {\r\n          currentKaraokeButton.unsubscribe();\r\n          currentKaraokeButton = null;\r\n        }\r\n        bus.publish(\"resetLyrics\");\r\n        bus.publish(\"resetProgress\");\r\n      });\r\n\r\n      this.audioElem.addEventListener(\"timeupdate\", () => {\r\n        bus.publish(\"timeUpdate\", this.audioElem.currentTime);\r\n      });\r\n\r\n      this.audioElem.addEventListener(\"loadedmetadata\", () => {\r\n        bus.publish(\"songDuration\", this.audioElem.duration);\r\n      });\r\n\r\n      this.audioElem.addEventListener(\"ended\", () => {\r\n        bus.publish(\"songEnded\");\r\n      });\r\n    }\r\n\r\n    loadSong(url) {\r\n      this.audioElem.innerHTML = \"\";\r\n      const source = document.createElement(\"source\");\r\n      source.src = url;\r\n      source.type = \"audio\/mpeg\";\r\n      this.audioElem.appendChild(source);\r\n      this.audioElem.load();\r\n      this.audioElem.currentTime = 0;\r\n    }\r\n  }\r\n\r\n  class Lyrics {\r\n    constructor(divLyrics, lyrics) {\r\n      this.divLyrics = divLyrics;\r\n      this.lyrics = lyrics;\r\n      this.previousIndex = -1;\r\n\r\n      this.addLyrics();\r\n\r\n      \/\/ Bind callbacks so we can unsubscribe later\r\n      this.updateFn = (time) => this.update(time);\r\n      this.resetFn = () => this.reset();\r\n\r\n      bus.subscribe(\"timeUpdate\", this.updateFn);\r\n      bus.subscribe(\"resetLyrics\", this.resetFn);\r\n    }\r\n\r\n    addLyrics() {\r\n      this.lyrics.forEach(item => {\r\n        const li = document.createElement(\"li\");\r\n        li.className = \"karaoke__lyrics-item\";\r\n        li.innerHTML = item.text;\r\n        this.divLyrics.appendChild(li);\r\n      });\r\n    }\r\n\r\n    unsubscribe() {\r\n      bus.events[\"timeUpdate\"] = bus.events[\"timeUpdate\"].filter(fn => fn !== this.updateFn);\r\n      bus.events[\"resetLyrics\"] = bus.events[\"resetLyrics\"].filter(fn => fn !== this.resetFn);\r\n    }\r\n\r\n    update(currentTime) {\r\n      const children = this.divLyrics.children;\r\n\r\n      this.lyrics.forEach((item, index) => {\r\n        if (currentTime >= item.start && currentTime <= item.end) {\r\n          const currentElement = children[index];\r\n          currentElement.classList.add(\"karaoke__lyrics-item--selected\");\r\n\r\n          if (this.previousIndex >= 0 && this.previousIndex !== index) {\r\n            children[this.previousIndex].classList.remove(\"karaoke__lyrics-item--selected\");\r\n          }\r\n\r\n          if (this.previousIndex !== index) {\r\n            currentElement.scrollIntoView({\r\n              behavior: \"smooth\",\r\n              block: \"center\",\r\n            });\r\n          }\r\n\r\n          this.previousIndex = index;\r\n        }\r\n      });\r\n    }\r\n\r\n    reset() {\r\n      Array.from(this.divLyrics.children).forEach(li => li.classList.remove(\"karaoke__lyrics-item--selected\"));\r\n      this.previousIndex = -1;\r\n      window.scrollTo({ top: 0, behavior: \"smooth\" });\r\n    }\r\n  }\r\n\r\n  class ProgressBar {\r\n    constructor(progressLine, progressFill, audioElem) {\r\n      this.lineElem = progressLine;\r\n      this.fillElem = progressFill;\r\n      this.audioElem = audioElem;\r\n      this.duration = 1;\r\n\r\n      bus.subscribe(\"songDuration\", dur => this.duration = dur);\r\n      bus.subscribe(\"timeUpdate\", time => this.update(time));\r\n      bus.subscribe(\"songEnded\", () => this.reset());\r\n\r\n      this.lineElem.addEventListener(\"click\", e => this.seek(e));\r\n    }\r\n\r\n    update(currentTime) {\r\n      const pct = (currentTime \/ this.duration) * 100;\r\n      this.fillElem.style.width = `${pct}%`;\r\n    }\r\n\r\n    reset() {\r\n      this.fillElem.style.width = \"0%\";\r\n    }\r\n\r\n    seek(e) {\r\n      const rect = this.lineElem.getBoundingClientRect();\r\n      const clickX = e.clientX - rect.left;\r\n      const pct = clickX \/ rect.width;\r\n      const newTime = pct * this.duration;\r\n      this.audioElem.currentTime = newTime;\r\n      bus.publish(\"timeUpdate\", newTime);\r\n    }\r\n  }\r\n\r\n  class KaraokeButton {\r\n    constructor(button, songData) {\r\n      this.button = button;\r\n      this.songData = songData;\r\n      this.isPlaying = false;\r\n\r\n      this.handleClick = this.handleClick.bind(this);\r\n      this.onPlay = this.onPlay.bind(this);\r\n      this.onPause = this.onPause.bind(this);\r\n      this.onEnded = this.onEnded.bind(this);\r\n\r\n      this.button.addEventListener(\"click\", this.handleClick);\r\n\r\n      bus.subscribe(\"play\", this.onPlay);\r\n      bus.subscribe(\"pause\", this.onPause);\r\n      bus.subscribe(\"songEnded\", this.onEnded);\r\n    }\r\n\r\n    handleClick() {\r\n      const label = this.button.textContent;\r\n\r\n      if (label.includes(\"RANDOM\")) {\r\n        \/\/ Pick a random song and start karaoke\r\n        if (songsList.length > 0) {\r\n          const randomIndex = Math.floor(Math.random() * songsList.length);\r\n          const randomSong = songsList[randomIndex];\r\n          startKaraoke(randomSong);\r\n        }\r\n      } else if (label.includes(\"PAUSE\")) {\r\n        bus.publish(\"pause\");\r\n      } else if (label.includes(\"RESUME\")) {\r\n        bus.publish(\"play\");\r\n      } else if (label.includes(\"RESTART\")) {\r\n        bus.publish(\"loadSong\", this.songData.audio);\r\n        bus.publish(\"play\");\r\n        bus.publish(\"resetLyrics\");\r\n        bus.publish(\"resetProgress\");\r\n      }\r\n    }\r\n\r\n    onPlay() {\r\n      this.isPlaying = true;\r\n      this.button.textContent = \"\\u23F8 PAUSE\";\r\n    }\r\n\r\n    onPause() {\r\n      this.isPlaying = false;\r\n      this.button.textContent = \"\\u25B6 RESUME\";\r\n    }\r\n\r\n    onEnded() {\r\n      this.isPlaying = false;\r\n      this.button.textContent = \"\\u{1f3b2} RANDOM\";\r\n    }\r\n\r\n    unsubscribe() {\r\n      bus.events[\"play\"] = bus.events[\"play\"].filter(fn => fn !== this.onPlay);\r\n      bus.events[\"pause\"] = bus.events[\"pause\"].filter(fn => fn !== this.onPause);\r\n      bus.events[\"songEnded\"] = bus.events[\"songEnded\"].filter(fn => fn !== this.onEnded);\r\n\r\n      this.button.removeEventListener(\"click\", this.handleClick);\r\n    }\r\n  }\r\n\r\n\r\n  \/\/ -------------------------------\r\n  \/\/ Load Songs from JASS\r\n  \/\/ -------------------------------\r\nconst lyricsContainer = document.querySelector(\".karaoke__lyrics\");\r\nconst audioElem = document.querySelector(\".karaoke__audio\");\r\nconst progressLine = document.querySelector(\".karaoke__progress-line\");\r\nconst progressFill = document.querySelector(\".karaoke__progress-fill\");\r\nconst playButton = document.getElementById(\"karaoke-button\");\r\nconst songListContainer = document.getElementById(\"song-list\");\r\n\r\nlet audioPlayer, progressBar;\r\nlet currentLyricsComponent, currentKaraokeButton;\r\n\r\n\/\/ Load songs and build clickable list\r\nshowLoader();\r\n\r\nfetch(\"https:\/\/cdn.jsdelivr.net\/gh\/skujp\/karaoke@v1.0.5\/songs.jas\")\r\n  .then(r => r.text())\r\n  .then(t => {\r\n    try {\r\n      return JSON.parse(t);\r\n    } catch (e) {\r\n      console.error('Invalid JSON in songs.jas:', e);\r\n      return null;\r\n    }\r\n  })\r\n  .then(songList => {\r\n    hideLoader();\r\n    songsList = songList; \/\/ Save globally\r\n\r\n    songList.forEach((song, index) => {\r\n      const btn = document.createElement(\"button\");\r\n      btn.className = \"karaoke__control-button\";\r\n      btn.style.margin = \"10px\";\r\n      btn.innerHTML = '<span class=\"mic-icon\">\\u{1f3a4}<\/span> ' + song.title;\r\n\r\n      \/\/ Store reference to the button\r\n      song._buttonRef = btn;\r\n\r\n      btn.addEventListener(\"click\", () => {\r\n        startKaraoke(song);\r\n      });\r\n\r\n      songListContainer.appendChild(btn);\r\n    });\r\n\r\n    \/\/ Show karaoke button with RANDOM label\r\n    const playButton = document.getElementById(\"karaoke-button\");\r\n    playButton.style.display = \"inline-block\";\r\n    playButton.innerHTML = '<span class=\"dice\">\\u{1f3b2}<\/span> RANDOM';\r\n\r\n    \/\/ Init KaraokeButton with no song yet\"\r\n    currentKaraokeButton = new KaraokeButton(playButton, null);\r\n  })\r\n  .catch(err => {\r\n    hideLoader();\r\n    console.error(\"Error loading songs:\", err);\r\n    songListContainer.innerHTML = `<p style=\"color:red;\">Failed to load songs.<\/p>`;\r\n  });\r\n\r\nfunction startKaraoke(songData) {\r\n  showLoader();\r\n\r\n  fetch(\"https:\/\/cdn.jsdelivr.net\/gh\/skujp\/karaoke@v1.0.5\/\" + songData.lyrics)\r\n    .then(r => r.text())\r\n    .then(t => {\r\n      try {\r\n        return JSON.parse(t);\r\n      } catch (e) {\r\n        console.error('Invalid JSON in lyrics:', e);\r\n        return null;\r\n      }\r\n    })\r\n    .then(lyricsData => {\r\n      hideLoader();\r\n\r\n      bus.publish(\"unloadSong\");\r\n\r\n      lyricsContainer.innerHTML = \"\";\r\n\r\n      \/\/ Load song + lyrics\r\n      if (!audioPlayer) {\r\n        audioPlayer = new AudioPlayer(audioElem);\r\n        progressBar = new ProgressBar(progressLine, progressFill, audioElem);\r\n      }\r\n\r\n      currentLyricsComponent = new Lyrics(lyricsContainer, lyricsData);\r\n\r\n      \/\/ Unsubscribe previous button\r\n      if (currentKaraokeButton) {\r\n        currentKaraokeButton.unsubscribe();\r\n      }\r\n\r\n      currentKaraokeButton = new KaraokeButton(playButton, songData);\r\n\r\n      bus.publish(\"loadSong\", atob(songData.audio));\r\n      bus.publish(\"play\");\r\n\r\n      \/\/ Highlight active song button\r\n      songsList.forEach(song => {\r\n        if (song._buttonRef) {\r\n          song._buttonRef.classList.toggle(\"active\", song === songData);\r\n        }\r\n      });\r\n\r\n    })\r\n    .catch(err => {\r\n      hideLoader();\r\n      console.error(\"Error loading lyrics:\", err);\r\n    });\r\n}\r\n\r\n\/\/ search functionality\r\nconst searchInput = document.getElementById(\"song-search\");\r\nconst noResults = document.getElementById(\"no-results\");\r\n\r\n\/\/ Debounce utility to limit how often filtering runs\r\nfunction debounce(fn, delay) {\r\n  let timeout;\r\n  return (...args) => {\r\n    clearTimeout(timeout);\r\n    timeout = setTimeout(() => fn(...args), delay);\r\n  };\r\n}\r\n\r\nconst filterSongs = () => {\r\n  const query = searchInput.value.trim().toLowerCase();\r\n  let matchCount = 0;\r\n\r\n  songsList.forEach(song => {\r\n    const btn = song._buttonRef;\r\n    const isMatch = song.title.toLowerCase().includes(query);\r\n    btn.style.display = isMatch ? \"inline-block\" : \"none\";\r\n    if (isMatch) matchCount++;\r\n  });\r\n\r\n  \/\/ Show or hide \"no results\" message\r\n  noResults.style.display = matchCount === 0 ? \"block\" : \"none\";\r\n};\r\n\r\nsearchInput.addEventListener(\"input\", debounce(filterSongs, 200));\r\n\r\n\r\n\/\/ pallete picker\r\nconst paletteBtn = document.getElementById('palette-btn');\r\nconst colorPicker = document.getElementById('bg-color-picker');\r\nconst karaoke = document.getElementById('karaoke');\r\n\r\n\/\/ When user clicks the palette icon, trigger the hidden color input\r\npaletteBtn.addEventListener('click', () => {\r\n  colorPicker.click();\r\n});\r\n\r\n\/\/ When user selects a color, change the karaoke background\r\ncolorPicker.addEventListener('input', (e) => {\r\n  const color = e.target.value;\r\n  karaoke.style.background = color;\r\n});\r\n\r\n});\r\n\r\n\r\n<\/script>","protected":false},"excerpt":{"rendered":"No matching songs found. &#127925; &#x1F3A8; &#038;#x1f3b2 RANDOM","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/pages\/419"}],"collection":[{"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/comments?post=419"}],"version-history":[{"count":0,"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/pages\/419\/revisions"}],"wp:attachment":[{"href":"https:\/\/nichimofoods.com\/en\/wp-json\/wp\/v2\/media?parent=419"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}