ffmml 0.1.2

Famicon (a.k.a. NES) Flavored Music Macro Language
Documentation
<html>
  <head>
    <meta charset="utf-8">
    <link rel="icon" type="image/png" href="icon-32x32.png" sizes="32x32" />
    <link rel="icon" type="image/png" href="icon-64x64.png" sizes="64x64" />
    <link rel="icon" type="image/png" href="icon-128x128.png" sizes="128x128" />
    <link rel="icon" type="image/png" href="icon-256x256.png" sizes="256x256" />
    <link rel="manifest" href="manifest.json">
    <link href="./primer.css" rel="stylesheet" />
    <title>FFMML Player</title>
  </head>
  <body>
    <div class="m-3" align="center">
      <h1>FFMML Player</h1>
      <a href="https://www.nesdev.org/mck_guide_v1.0.txt">MML (MCK) Guide</a> |
      <a href="https://github.com/sile/ffmml">FFMML GitHub Repository</a>
      <br /><br />
      <div>
        <textarea id="mml" rows="20" cols="60" class="text-mono"></textarea>
      </div>
      <br />
      <div>
        <input value="Play Music" type="button" onclick="playAudio()" class="btn">
        &nbsp;&nbsp;
        <input value="Update URL" type="button" onclick="updateUrl()" class="btn">
        &nbsp;&nbsp;
        <input value="Download .wav" type="button" onclick="downloadWav()" class="btn">
        &nbsp;&nbsp;
      <input value="Share" type="button" onclick="share()" class="btn">
      </div>
      <br />
      <div>
        <textarea id="errorMessageArea"
                  class="text-mono"
                  style="color:red; font-weight:bold; border:none; resize:none; display:none"
                  readonly rows="6" cols="80">
        </textarea>
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/pagurus@0.6.1/dist/pagurus.min.js"></script>
    <script>
      const params = new URLSearchParams(window.location.search);
      if (params.get('mml')) {
          document.getElementById('mml').value =  params.get('mml');
      }

      var system = undefined;
      var game = undefined;

      function parseScript() {
          const mml = document.getElementById("mml").value;
          game.command(system, "parseScript", new TextEncoder().encode(mml));
      }

      function playAudio() {
          try {
              parseScript();
              game.command(system, "playAudio", new Uint8Array(0));
              clearErrorMessage();
          } catch (e) {
              console.warn(e);
              setErrorMessage(JSON.parse(e.message).message);
          }
      }

      function updateUrl() {
          try {
              parseScript();
              const mml = document.getElementById("mml").value;
              const params = new URLSearchParams();
              params.set('mml', mml);
              window.location.search = params.toString();
              clearErrorMessage();
          } catch (e) {
              setErrorMessage(JSON.parse(e.message).message);
          }
      }

      function share() {
          try {
              parseScript();

              const mml = document.getElementById("mml").value;
              const params = new URLSearchParams();
              params.set('mml', mml);
              const url = new URL(window.location.href);
              url.params = params.toString();
              console.log("```\n" + mml.trim() + "\n```");
              navigator.share({
                  title: getTitle(),
                  text: "```\n" + mml.trim() + "\n```\n",
                  url,
              });
          } catch (e) {
              setErrorMessage(JSON.parse(e.message).message);
          }
      }

      function getTitle() {
          const titleBytes = game.query(system, "title");
          if (titleBytes.length == 0) {
              return `mml-${now()}`;
          } else {
              return new TextDecoder('utf8').decode(titleBytes);
          }
      }

      function downloadWav() {
          let blob;
          try {
              parseScript();
              const data = game.query(system, "exportWav");
              blob = new Blob([data], {type: 'audio/x-wav'});
              clearErrorMessage();
          } catch (e) {
              console.warn(e);
              setErrorMessage(JSON.parse(e.message).message);
              return;
          }

          const element = document.createElement("a");
          element.download = `${getTitle()}.wav`;
          element.href = URL.createObjectURL(blob);
          element.click();
      }

      function clearErrorMessage() {
          const errorMessage = document.getElementById("errorMessageArea");
          errorMessage.style.display = "none";
          errorMessage.value = "";
      }

      function setErrorMessage(msg) {
          const errorMessage = document.getElementById("errorMessageArea");
          errorMessage.style.display = "block";
          errorMessage.value = "ERROR: " + msg;
      }

      function now() {
          return new Intl.DateTimeFormat(
              [],
              {
                  year:   'numeric',
                  month:  '2-digit',
                  day:    '2-digit',
                  hour:   '2-digit',
                  minute: '2-digit',
                  second: '2-digit',
              }
          ).format(new Date()).replaceAll(/[:/]/g, '').replace(' ', '_');
      }

      Pagurus.Game.load("ffmml.wasm").then(async gameObject => {
          game = gameObject;
          system = await Pagurus.System.create(game.memory);

          game.initialize(system);
          while (true) {
              const event = await system.nextEvent();
              try {
                  if (!game.handleEvent(system, event)) {
                      break;
                  }
              } catch (e) {
                  console.warn(e);
                  setErrorMessage(JSON.parse(e.message).message);
              }
          }
      });
    </script>
  </body>
</html>