{"id":145,"date":"2026-09-03T18:23:43","date_gmt":"2026-09-03T06:23:43","guid":{"rendered":"https:\/\/xpd.co.nz\/?p=145"},"modified":"2026-09-03T21:16:12","modified_gmt":"2026-09-03T09:16:12","slug":"test-amiga-music","status":"publish","type":"post","link":"https:\/\/xpd.co.nz\/index.php\/2026\/09\/03\/test-amiga-music\/","title":{"rendered":"Test &#8211; Amiga music"},"content":{"rendered":"\n<!--\n  MOD\/Tracker Jukebox \u00e2\u20ac\u201d for WordPress \"Custom HTML\" block\n  ---------------------------------------------------------------\n  Plays a folder full of .mod\/.xm\/.s3m\/.it files via chiptune3\n  (libopenmpt\/WASM). Reads the track list from list-mods.php, which\n  must live in the SAME folder as your tracker files.\n\n  SETUP:\n  1. Put list-mods.php in your mods folder, e.g.:\n       \/wp-content\/uploads\/mods\/list-mods.php\n       \/wp-content\/uploads\/mods\/track1.mod\n       \/wp-content\/uploads\/mods\/track2.mod\n  2. Set MODS_FOLDER_URL below to that folder's URL.\n  3. Paste this whole block into a Custom HTML block in WP.\n\n  Features: shuffle\/random, next\/prev, autoplay-through-playlist,\n  click any track in the list to jump to it.\n-->\n\n<div class=\"mod-jukebox-widget\" style=\"max-width:480px;font-family:sans-serif;border:1px solid #ccc;border-radius:8px;padding:16px;background:#f9f9f9;\">\n  <div style=\"font-weight:bold;margin-bottom:8px;\" id=\"jb-title\">Loading playlist\u00e2\u20ac\u00a6<\/div>\n\n  <div style=\"display:flex;gap:8px;align-items:center;margin-bottom:10px;\">\n    <button id=\"jb-prev\" style=\"padding:6px 12px;cursor:pointer;\" disabled>\u00e2\u008f\u00ae Prev<\/button>\n    <button id=\"jb-play\" style=\"padding:6px 14px;cursor:pointer;\" disabled>\u00e2\u2013\u00b6 Play<\/button>\n    <button id=\"jb-next\" style=\"padding:6px 12px;cursor:pointer;\" disabled>Next \u00e2\u008f\u00ad<\/button>\n    <button id=\"jb-shuffle\" style=\"padding:6px 12px;cursor:pointer;margin-left:auto;\" title=\"Toggle shuffle\">\u00f0\u0178\u201d\u20ac<\/button>\n  <\/div>\n\n  <div style=\"font-size:0.85em;color:#666;margin-bottom:8px;\" id=\"jb-status\">initialising\u00e2\u20ac\u00a6<\/div>\n\n  <ul id=\"jb-list\" style=\"list-style:none;margin:0;padding:0;max-height:200px;overflow-y:auto;border-top:1px solid #ddd;\">\n  <\/ul>\n<\/div>\n\n<script type=\"module\">\n  import { ChiptuneJsPlayer } from 'https:\/\/DrSnuggles.github.io\/chiptune\/chiptune3.js';\n\n  \/\/ ---- CONFIG ----\n  const MODS_FOLDER_URL = \"https:\/\/www.xpd.co.nz\/wp-content\/uploads\/mods\/\";\n  \/\/ -----------------\n\n  const titleEl = document.getElementById(\"jb-title\");\n  const statusEl = document.getElementById(\"jb-status\");\n  const listEl = document.getElementById(\"jb-list\");\n  const playBtn = document.getElementById(\"jb-play\");\n  const prevBtn = document.getElementById(\"jb-prev\");\n  const nextBtn = document.getElementById(\"jb-next\");\n  const shuffleBtn = document.getElementById(\"jb-shuffle\");\n\n  const chiptune = new ChiptuneJsPlayer();\n\n  let playlist = [];       \/\/ filenames\n  let currentIndex = -1;\n  let isPlaying = false;\n  let shuffleOn = false;\n  let ctxReady = false;\n\n  function renderList() {\n    listEl.innerHTML = \"\";\n    playlist.forEach((name, i) => {\n      const li = document.createElement(\"li\");\n      li.textContent = name;\n      li.style.padding = \"5px 4px\";\n      li.style.cursor = \"pointer\";\n      li.style.borderBottom = \"1px solid #eee\";\n      li.style.background = i === currentIndex ? \"#e8e8e8\" : \"transparent\";\n      li.style.fontWeight = i === currentIndex ? \"bold\" : \"normal\";\n      li.addEventListener(\"click\", () => playIndex(i));\n      listEl.appendChild(li);\n    });\n  }\n\n  function pickNextIndex() {\n    if (playlist.length === 0) return -1;\n    if (shuffleOn) {\n      if (playlist.length === 1) return 0;\n      let next;\n      do { next = Math.floor(Math.random() * playlist.length); }\n      while (next === currentIndex);\n      return next;\n    }\n    return (currentIndex + 1) % playlist.length;\n  }\n\n  function pickPrevIndex() {\n    if (playlist.length === 0) return -1;\n    if (shuffleOn) return pickNextIndex(); \/\/ shuffle has no real \"previous\"\n    return (currentIndex - 1 + playlist.length) % playlist.length;\n  }\n\n  async function playIndex(i) {\n    if (i < 0 || i >= playlist.length) return;\n    try {\n      if (chiptune.context && chiptune.context.state === \"suspended\") {\n        await chiptune.context.resume();\n      }\n      currentIndex = i;\n      const name = playlist[i];\n      statusEl.textContent = \"loading \" + name + \"\u00e2\u20ac\u00a6\";\n      titleEl.textContent = name;\n      renderList();\n      await chiptune.load(MODS_FOLDER_URL + encodeURIComponent(name));\n      isPlaying = true;\n      statusEl.textContent = \"playing\";\n      playBtn.textContent = \"\u00e2\u008f\u00b8 Pause\";\n    } catch (err) {\n      statusEl.textContent = \"error loading \" + playlist[i];\n      console.error(err);\n    }\n  }\n\n  \/\/ Autoplay next track when current one finishes\n  chiptune.onEnded(() => {\n    isPlaying = false;\n    const next = pickNextIndex();\n    if (next !== -1) playIndex(next);\n  });\n\n  chiptune.onError((err) => {\n    statusEl.textContent = \"playback error\";\n    console.error(\"chiptune error:\", err);\n  });\n\n  chiptune.onInitialized(() => {\n    ctxReady = true;\n    statusEl.textContent = \"ready\";\n  });\n\n  playBtn.addEventListener(\"click\", async () => {\n    if (currentIndex === -1) {\n      \/\/ nothing loaded yet \u00e2\u20ac\u201d start at track 0 (or random if shuffle is on)\n      playIndex(shuffleOn ? pickNextIndex() : 0);\n      return;\n    }\n    if (chiptune.context && chiptune.context.state === \"suspended\") {\n      await chiptune.context.resume();\n    }\n    chiptune.togglePause();\n    isPlaying = !!chiptune.currentPlayingNode;\n    statusEl.textContent = isPlaying ? \"playing\" : \"paused\";\n    playBtn.textContent = isPlaying ? \"\u00e2\u008f\u00b8 Pause\" : \"\u00e2\u2013\u00b6 Play\";\n  });\n\n  nextBtn.addEventListener(\"click\", () => playIndex(pickNextIndex()));\n  prevBtn.addEventListener(\"click\", () => playIndex(pickPrevIndex()));\n\n  shuffleBtn.addEventListener(\"click\", () => {\n    shuffleOn = !shuffleOn;\n    shuffleBtn.style.opacity = shuffleOn ? \"1\" : \"0.5\";\n    shuffleBtn.title = shuffleOn ? \"Shuffle: on\" : \"Shuffle: off\";\n  });\n  shuffleBtn.style.opacity = \"0.5\"; \/\/ starts off\n\n  \/\/ ---- Load the playlist from list-mods.php ----\n  fetch(MODS_FOLDER_URL + \"list-mods.php\")\n    .then((r) => {\n      if (!r.ok) throw new Error(\"failed to fetch track list: \" + r.status);\n      return r.json();\n    })\n    .then((tracks) => {\n      playlist = tracks;\n      if (playlist.length === 0) {\n        titleEl.textContent = \"No tracks found\";\n        statusEl.textContent = \"check MODS_FOLDER_URL and list-mods.php\";\n        return;\n      }\n      titleEl.textContent = playlist.length + \" tracks loaded\";\n      statusEl.textContent = \"ready \u00e2\u20ac\u201d press play\";\n      renderList();\n      playBtn.disabled = false;\n      nextBtn.disabled = false;\n      prevBtn.disabled = false;\n    })\n    .catch((err) => {\n      titleEl.textContent = \"Couldn't load playlist\";\n      statusEl.textContent = err.message;\n      console.error(err);\n    });\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Loading playlist\u00e2\u20ac\u00a6 \u00e2\u008f\u00ae Prev \u00e2\u2013\u00b6 Play Next \u00e2\u008f\u00ad \u00f0\u0178\u201d\u20ac initialising\u00e2\u20ac\u00a6<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-145","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/posts\/145","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/comments?post=145"}],"version-history":[{"count":4,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/posts\/145\/revisions"}],"predecessor-version":[{"id":149,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/posts\/145\/revisions\/149"}],"wp:attachment":[{"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/media?parent=145"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/categories?post=145"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/xpd.co.nz\/index.php\/wp-json\/wp\/v2\/tags?post=145"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}