[
  {
    "path": ".gitignore",
    "content": "bower_components\nnode_modules\n*.log\n.DS_Store\nbundle.js\n"
  },
  {
    "path": ".npmignore",
    "content": "bower_components\nnode_modules\n*.log\n.DS_Store\nbundle.js\ntest\ntest.js\ndemo/\n.npmignore\nLICENSE.md"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\nCopyright (c) 2017 Matt DesLauriers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE\nOR OTHER DEALINGS IN THE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Web Audio Synthesis & Visualization\n\nThis repository includes resources & course notes for those attending my _Web Audio Synthesis & Visualization_ workshop with Frontend Masters, demonstrating the raw Web Audio API, [p5.js](https://p5js.org) for rendering, and [Tone.js](https://tonejs.github.io) for synthesis.\n\n<center><a href=\"https://web-audio-demos.glitch.me\"><img src=\"docs/images/screen.png\" width=\"75%\" /></a></center>\n\n# Contents\n\n- ✨ [Course Demos](#course-demos)\n\n- 🔧 [Tools](#tools)\n\n- ✂️️ [Code Snippets](#code-snippets)\n\n- 📖 [Setup](#setup)\n\n- ✨ [Further Reading](#further-reading)\n\n# Course Demos\n\n- 📚 Collections\n\n  - 🔈 **[web-audio-demos.glitch.me](https://web-audio-demos.glitch.me/)** — playback and visualization examples with pure WebAudio\n\n  - 🔈 **[tone-demos.glitch.me](https://tone-demos.glitch.me)** — synthesis and other examples with Tone.js\n\n  - 🎨 **[p5-demos.glitch.me](https://p5-demos.glitch.me)** — examples with p5.js\n\n# Tools\n\nHere is a list of tools and libraries that will be used during the workshop.\n\n| Tool                                | Documentation                                                              | Version                                                               | Description                                                                        |\n| ----------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| _A browser_                         |                                                                            |                                                                       | A modern browser, [Chrome](https://www.google.com/chrome/) is recommended          |\n| [Glitch](https://glitch.com)        | [Help](https://glitch.com/help/)                                           |                                                                       | An online platform for editing & sharing JavaScript projects                       |\n| _Web Audio API_                     | [API Docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) |                                                                       | This API is built into modern browsers and allows us to work with audio and sound. |\n| [Tone.js](https://tonejs.github.io) | [API Docs](https://tonejs.github.io/docs/)                                 | [13.8.25](https://unpkg.com/tone@13.8.25/build/Tone.js)               | A JavaScript audio library for playing synths and sounds                           |\n| [p5.js](https://p5js.org)           | [API Docs](https://p5js.org/reference/)                                    | [0.9.0](https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js) | A JavaScript graphics library for creative coding                                  |\n\n# Just Starting Out\n\nThis workshop assumes you are comfortable with JavaScript and ES6 syntax, and instead focuses just on the audio side of things. If you're new to JavaScript, you might want to begin your journey below:\n\n- [JavaScript For Cats](http://jsforcats.com/)\n\nAlso great is Daniel Shiffman's video series, which often uses p5.js:\n\n- [Programming from A to Z](https://shiffman.net/a2z/)\n\nA more comprehensive guide on the JavaScript language can be found here:\n\n- [The Modern JavaScript Tutorial](https://javascript.info/)\n\nAnd here's a useful cheat sheet to use as a reference:\n\n- [Interactive JavaScript Cheat Sheet](https://htmlcheatsheet.com/js/)\n\n# Code Snippets\n\nI've also included a small \"recipes\" document that you can use as a reference if you are forgetting some of the patterns and recipes discussed during the workshop.\n\n- [Code Snippets](./docs/snippets.md)\n\n# Setup\n\nIf you want to run locally (without internet), clone this repo into a new folder, then use Node.js to cd into the folder and run the following to install dependencies:\n\n```sh\nnpm install\n```\n\nNow, start the development server:\n\n```sh\nnpm start\n```\n\nEdit files in `src/js` and see them reflected in the browser.\n\n# Further Reading\n\nMore links to web audio, creative coding, and more:\n\n- Resources & Tutorials\n\n  - [awesome-web-audio](https://github.com/notthetup/awesome-webaudio) — A list that includes resources, books, and more on Web Audio\n\n  - [The Coding Train](https://thecodingtrain.com) with Daniel Shiffman\n\n  - [Creative Coding with Canvas & WebGL](https://frontendmasters.com/courses/canvas-webgl/) — My own course, if you want to continue exploring the world of creative coding and generative art\n\n  - [awesome-audio-visualization](https://github.com/willianjusten/awesome-audio-visualization) — A large list of interesting Web Audio visualizers\n\n  - [awesome-creative-coding](https://github.com/terkelg/awesome-creative-coding) — A large list of resources\n\n- Math\n\n  - [Sine / Cosine Reference](https://www.mathsisfun.com/algebra/trig-interactive-unit-circle.html)\n\n  - [Sine and Cosine Calculator](https://www.desmos.com/calculator/hlqxvc6hho)\n\n  - [math-as-code](https://github.com/Jam3/math-as-code) — A cheat sheet for mathematical notation in code form\n\n- Learning Audio\n\n  - [Learning Synths by Ableton](https://learningsynths.ableton.com/)\n\n  - [Learning Music by Ableton](https://learningmusic.ableton.com/index.html)\n\n  - [Music Theory](https://www.lightnote.co/)\n\n- Fun Web Audio Sites\n\n  - [generative.fm](https://play.generative.fm/browse)\n\n  - [Patatap](https://patatap.com/)\n\n  - [Blob Opera](https://artsandculture.google.com/experiment/blob-opera/AAHWrq360NcGbw?hl=en)\n\n  - [Sounds of the Pub](https://soundsofthepub.com/)\n\n  - [Pink Trombone](https://dood.al/pinktrombone/)\n\n- Tools\n\n  - [Spectrum Analyser](http://spectrum.surge.sh/) — see the frequency spectrum of an MP3 file\n\n# License\n\nMIT, see [LICENSE.md](./LICENSE.md) for details.\n"
  },
  {
    "path": "docs/snippets.md",
    "content": "#### <sup>:closed_book: [Web Audio Synthesis & Visualization](../README.md) → Snippets</sup>\n\n---\n\n# Snippets\n\nHere you will find some 'recipes' and patterns that we'll be using during the workshop.\n\n## Contents\n\n- [Playing an Audio Tag](#creating-an-audio-tag)\n- [Loading an Audio Buffer](#loading-an-audio-buffer)\n- [Playing an Audio Buffer](#playing-an-audio-buffer)\n- [Analysing Audio Waveform](#analysing-audio-waveform)\n- [Analysing Audio Frequency](#analysing-audio-frequency)\n- [Root Mean Squared Metering](#root-mean-squared-metering)\n- [Indexing into the Frequency Array](#indexing-into-the-frequency-array)\n- [Disabling Builtin Play/Pause Controls](#disabling-builtin-playpause-controls)\n\n## Creating an Audio Tag\n\n```js\n// Create <audio> tag\nconst audio = document.createElement(\"audio\");\n\n// set URL to the MP3 within your Glitch.com assets\naudio.src = \"path/to/music.mp3\";\n\n// To play audio through Glitch.com CDN\naudio.crossOrigin = \"Anonymous\";\n\n// Optional: enable looping so the audio never stops\naudio.loop = true;\n\n// Play audio\naudio.play();\n\n// If it's not already playing, resume audio context\naudioContext.resume();\n```\n\n## Loading an Audio Buffer\n\n```js\nlet audioContext;\nlet audioBuffer;\n\nasync function loadSound() {\n  // Re-use the same context if it exists\n  if (!audioContext) {\n    audioContext = new AudioContext();\n  }\n\n  // Re-use the audio buffer as a source\n  if (!audioBuffer) {\n    // Fetch MP3 from URL\n    const resp = await fetch(\"path/to/music.mp3\");\n\n    // Turn into an array buffer of raw binary data\n    const buf = await resp.arrayBuffer();\n\n    // Decode the entire binary MP3 into an AudioBuffer\n    audioBuffer = await audioContext.decodeAudioData(buf);\n  }\n}\n```\n\n## Playing an Audio Buffer\n\nThis relies on the `loadSound` function just described previously, as you can only play an audio buffer once it's been loaded and decoded asynchronously.\n\n```js\nasync function playSound() {\n  // Ensure we are all loaded up\n  await loadSound();\n\n  // Ensure we are in a resumed state\n  await audioContext.resume();\n\n  // Now create a new \"Buffer Source\" node for playing AudioBuffers\n  const source = audioContext.createBufferSource();\n\n  // Connect to gain (which will be analyzed and also sent to destination)\n  source.connect(audioContext.destination);\n\n  // Assign the loaded buffer\n  source.buffer = audioBuffer;\n\n  // Start (zero = play immediately)\n  source.start(0);\n}\n```\n\n## Disabling Builtin Play/Pause Controls\n\nBrowsers, by default, will play/pause `<audio>` elements on keyboard controls, and also sometimes when you connect and disconnect bluetooth headphones. In many apps, you may want to override this.\n\n```js\n// just ignore this event\nnavigator.mediaSession.setActionHandler(\"pause\", () => {});\n```\n\n## Analysing Audio Waveform\n\n```js\nlet data;\nlet analyserNode;\n\nfunction setupAudio() {\n  /* ... create an audio 'source' node ... */\n\n  analyserNode = audioContext.createAnalyser();\n  signalData = new Float32Array(analyserNode.fftSize);\n\n  source.connect(analyserNode);\n}\n\nfunction draw() {\n  analyserNode.getFloatTimeDomainData(signalData);\n\n  /* now visualize ... */\n}\n```\n\n## Analysing Audio Frequency\n\n```js\nlet data;\nlet frequencyData;\n\nfunction setupAudio() {\n  /* ... create an audio 'source' node ... */\n\n  analyserNode = audioContext.createAnalyser();\n  frequencyData = new Float32Array(analyserNode.frequencyBinCount);\n\n  source.connect(analyserNode);\n}\n\nfunction draw() {\n  analyserNode.getFloatFrequencyData(frequencyData);\n\n  /* now visualize ... */\n}\n```\n\n## Root Mean Squared Metering\n\nStart with [Analysing Audio Waveform](#analysing-audio-waveform) snippet and then pass the data into the following function to get a signal between 0 and 1.\n\n```js\nfunction rootMeanSquaredSignal(data) {\n  let rms = 0;\n  for (let i = 0; i < data.length; i++) {\n    rms += data[i] * data[i];\n  }\n  return Math.sqrt(rms / data.length);\n}\n```\n\n## Indexing into the Frequency Array\n\nIf you have an array that represents a list of frequency bins (i.e. where the indices represent a frequency band in Hz and the array elements represent it's signal in Db) you can convert from Hz to an index and back like so:\n\n```js\n// Convert the frequency in Hz to an index in the array\nfunction frequencyToIndex(frequencyHz, sampleRate, frequencyBinCount) {\n  const nyquist = sampleRate / 2;\n  const index = Math.round((frequencyHz / nyquist) * frequencyBinCount);\n  return Math.min(frequencyBinCount, Math.max(0, index));\n}\n\n// Convert an index in a array to a frequency in Hz\nfunction indexToFrequency(index, sampleRate, frequencyBinCount) {\n  return (index * sampleRate) / (frequencyBinCount * 2);\n}\n```\n\n##\n\n#### <sup>[← Back to Documentation](../README.md)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"workshop-web-audio\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\"\n  },\n  \"devDependencies\": {\n    \"live-server\": \"^1.2.1\"\n  },\n  \"scripts\": {\n    \"start\": \"live-server src\"\n  },\n  \"keywords\": [],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git://github.com/mattdesl/workshop-web-audio.git\"\n  },\n  \"homepage\": \"https://github.com/mattdesl/workshop-web-audio\",\n  \"bugs\": {\n    \"url\": \"https://github.com/mattdesl/workshop-web-audio/issues\"\n  }\n}\n"
  },
  {
    "path": "src/01-play-mp3-stream.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/01-play-mp3-stream.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/02-play-mp3-buffer.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/02-play-mp3-buffer.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/03-gain.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/03-gain.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/04-waveform.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/04-waveform.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/05-waveform-advanced.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/05-waveform-advanced.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/06-meter.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/06-meter.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/07-meter-levels.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/07-meter-levels.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/08-frequency.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/08-frequency.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/09-frequency-advanced.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/09-frequency-advanced.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/10-tone-demo.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/10-tone-demo.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/11-tone-tap.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/11-tone-tap.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/12-tone-patatap.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/12-tone-patatap.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/13-tone-effects.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/13-tone-effects.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/14-tone-sequencer.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/14-tone-sequencer.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/15-tone-mp3-effects.html",
    "content": "<html>\n  <head>\n    <title>sketch</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <style>\n      body {\n        padding: 0;\n        margin: 0;\n        cursor: pointer;\n      }\n    </style>\n\n    <!-- An error helper tool for development only, you can omit for production -->\n    <script src=\"https://cdn.jsdelivr.net/npm/error-help@1.0.8/error-help.js\"></script>\n\n    <!--\n      We use unpkg instead of cdnjs as its recommended by tone.js docs\n    -->\n    <script src=\"https://unpkg.com/tone@13.8.25/build/Tone.js\"></script>\n\n    <!-- p5, you can use a newer version if you would rather -->\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js\"></script>\n\n    <!-- TODO: Change this to your own JS sketch file. -->\n    <script src=\"js/15-tone-mp3-effects.js\"></script>\n  </head>\n  <body></body>\n</html>\n"
  },
  {
    "path": "src/js/01-play-mp3-stream.js",
    "content": "let audioContext;\nlet audio;\n\nfunction mousePressed() {\n  if (!audioContext) {\n    // setup our audio\n    audioContext = new AudioContext();\n\n    // create new <audio> tag\n    audio = document.createElement(\"audio\");\n\n    // optional; enable audio looping\n    audio.loop = true;\n\n    // set the URL of the audio asset\n    audio.src = \"audio/piano.mp3\";\n\n    // trigger audio\n    audio.play();\n\n    const source = audioContext.createMediaElementSource(audio);\n\n    // wire the source to the 'speaker'\n    source.connect(audioContext.destination);\n  } else {\n    // stop the audio\n    audio.pause();\n    audioContext.close();\n    audioContext = audio = null;\n  }\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  fill(\"white\");\n  noStroke();\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (audioContext) {\n    polygon(width / 2, height / 2, dim * 0.1, 4, PI / 4);\n  } else {\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/02-play-mp3-buffer.js",
    "content": "/* eslint-disable */\n\nlet audioContext;\nlet audioBuffer;\n\nfunction mousePressed() {\n  playSound();\n}\n\nasync function loadSound() {\n  // Re-use the same context if it exists\n  if (!audioContext) {\n    audioContext = new AudioContext();\n  }\n\n  // Re-use the audio buffer as a source\n  if (!audioBuffer) {\n    // Fetch MP3 from URL\n    const resp = await fetch(\"audio/chime.mp3\");\n\n    // Turn into an array buffer of raw binary data\n    const buf = await resp.arrayBuffer();\n\n    // Decode the entire binary MP3 into an AudioBuffer\n    audioBuffer = await audioContext.decodeAudioData(buf);\n  }\n}\n\nasync function playSound() {\n  // Ensure we are all loaded up\n  await loadSound();\n\n  // Ensure we are in a resumed state\n  await audioContext.resume();\n\n  // Now create a new \"Buffer Source\" node for playing AudioBuffers\n  const source = audioContext.createBufferSource();\n\n  // Connect to destination\n  source.connect(audioContext.destination);\n\n  // Assign the loaded buffer\n  source.buffer = audioBuffer;\n\n  // Start (zero = play immediately)\n  source.start(0);\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  fill(\"white\");\n  noStroke();\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (mouseIsPressed) {\n    circle(width / 2, height / 2, dim * 0.1);\n  } else {\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/03-gain.js",
    "content": "let audioContext;\nlet audio;\nlet gainNode;\n\nfunction mousePressed() {\n  if (!audioContext) {\n    // Create a new audio context\n    audioContext = new AudioContext();\n\n    // Create <audio> tag\n    audio = document.createElement(\"audio\");\n\n    // set URL to the MP3 within your Glitch.com assets\n    audio.src = \"audio/piano.mp3\";\n\n    // To play audio through Glitch.com CDN\n    audio.crossOrigin = \"Anonymous\";\n\n    // Enable looping so the audio never stops\n    audio.loop = true;\n\n    // Play audio\n    audio.play();\n\n    // Create a \"Media Element\" source node\n    const source = audioContext.createMediaElementSource(audio);\n\n    // Create a gain for volume adjustment\n    gainNode = audioContext.createGain();\n\n    // wire source to gain\n    source.connect(gainNode);\n\n    // wire the gain -> speaker\n    gainNode.connect(audioContext.destination);\n  } else {\n    // Clean up our element and audio context\n    audio.pause();\n    audioContext.close();\n    audioContext = audio = null;\n  }\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  fill(\"white\");\n  noStroke();\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (audioContext) {\n    // Get a new volume based on mouse position\n    const volume = abs(mouseX - width / 2) / (width / 2);\n\n    // Schedule a gradual shift in value with a small time constant\n    gainNode.gain.setTargetAtTime(volume, audioContext.currentTime, 0.01);\n\n    // Draw a volume meter\n    rectMode(CENTER);\n    rect(width / 2, height / 2, dim * volume, dim * 0.05);\n  } else {\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/04-waveform.js",
    "content": "/* eslint-disable */\n\nlet audioContext;\nlet audioBuffer;\nlet analyserNode;\nlet analyserData;\nlet gainNode;\n\nfunction mousePressed() {\n  playSound();\n}\n\nasync function loadSound() {\n  // Re-use the same context if it exists\n  if (!audioContext) {\n    audioContext = new AudioContext();\n  }\n\n  // Re-use the audio buffer as a source\n  if (!audioBuffer) {\n    // Fetch MP3 from URL\n    const resp = await fetch(\"audio/chime.mp3\");\n\n    // Turn into an array buffer of raw binary data\n    const buf = await resp.arrayBuffer();\n\n    // Decode the entire binary MP3 into an AudioBuffer\n    audioBuffer = await audioContext.decodeAudioData(buf);\n  }\n\n  // Setup a master gain node and AnalyserNode\n  if (!gainNode) {\n    // Create a gain and connect to destination\n    gainNode = audioContext.createGain();\n\n    // Create an Analyser Node\n    analyserNode = audioContext.createAnalyser();\n\n    // Create a Float32 array to hold the data\n    analyserData = new Float32Array(analyserNode.fftSize);\n\n    // Connect the GainNode to the analyser\n    gainNode.connect(analyserNode);\n\n    // Connect GainNode to destination as well\n    gainNode.connect(audioContext.destination);\n  }\n}\n\nasync function playSound() {\n  // Snsure we are all loaded up\n  await loadSound();\n\n  // Snsure we are in a resumed state\n  await audioContext.resume();\n\n  // Now create a new \"Buffer Source\" node for playing AudioBuffers\n  const source = audioContext.createBufferSource();\n\n  // Connect to gain (which will be analyzed and also sent to destination)\n  source.connect(gainNode);\n\n  // Assign the loaded buffer\n  source.buffer = audioBuffer;\n\n  // Start (zero = play immediately)\n  source.start(0);\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(0, 0, 0);\n\n  if (analyserNode) {\n    noFill();\n    stroke(\"white\");\n\n    // get time domain data\n    analyserNode.getFloatTimeDomainData(analyserData);\n\n    beginShape();\n\n    for (let i = 0; i < analyserData.length; i++) {\n      // -1...1\n      const amplitude = analyserData[i];\n\n      const y = map(\n        amplitude,\n        -1,\n        1,\n        height / 2 - height / 4,\n        height / 2 + height / 4\n      );\n\n      const x = map(i, 0, analyserData.length - 1, 0, width);\n\n      vertex(x, y);\n    }\n\n    endShape();\n  } else {\n    fill(\"white\");\n    noStroke();\n    // Draw a play button\n    const dim = min(width, height);\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/05-waveform-advanced.js",
    "content": "/* eslint-disable */\n\nlet audioContext;\nlet analyserNode;\nlet analyserData;\nlet gainNode;\nlet audio;\nlet isFloat = false;\nlet interval;\n\nfunction mousePressed() {\n  // Only initiate audio upon a user gesture\n  if (!audioContext) {\n    const AudioContext = window.AudioContext || window.webkitAudioContext;\n    audioContext = new AudioContext();\n\n    // Optional:\n    // If the user inserts/removes bluetooth headphones or pushes\n    // the play/pause media keys, we can use the following to ignore the action\n    // navigator.mediaSession.setActionHandler(\"pause\", () => {});\n\n    // Make a stream source, i.e. MP3, microphone, etc\n    // In this case we choose an <audio> element\n    audio = document.createElement(\"audio\");\n\n    // Upon loading the audio, let's play it\n    audio.addEventListener(\n      \"canplay\",\n      () => {\n        // First, ensure the context is in a resumed state\n        audioContext.resume();\n        // Now, play the audio\n        audio.play();\n      },\n      { once: true }\n    );\n\n    // Loop audio\n    audio.loop = true;\n\n    // Set source\n    audio.crossOrigin = \"Anonymous\";\n    audio.src = \"audio/piano.mp3\";\n\n    // Connect source into the WebAudio context\n    const source = audioContext.createMediaElementSource(audio);\n    source.connect(audioContext.destination);\n\n    analyserNode = audioContext.createAnalyser();\n\n    // You can increase the detail to some power-of-two value\n    // This will give you more samples of data per second\n    const detail = 4;\n    analyserNode.fftSize = 2048 * detail;\n\n    isFloat = Boolean(analyserNode.getFloatTimeDomainData);\n    analyserData = new Float32Array(analyserNode.fftSize);\n    if (isFloat) {\n      // We can use float array for this, for higher detail\n      analyserTarget = new Float32Array(analyserData.length);\n    } else {\n      // We are stuck with byte array\n      analyserTarget = new Uint8Array(analyserData.length);\n      analyserTarget.fill(0xff / 2);\n    }\n\n    // connect source to analyser\n    source.connect(analyserNode);\n\n    // Only update the data every N fps\n    const fps = 12;\n    interval = setInterval(() => {\n      if (isFloat) {\n        analyserNode.getFloatTimeDomainData(analyserTarget);\n      } else {\n        analyserNode.getByteTimeDomainData(analyserTarget);\n      }\n    }, (1 / fps) * 1000);\n  } else {\n    // kill audio\n    audio.pause();\n    audioContext.close();\n    clearInterval(interval);\n    audioContext = analyserNode = null;\n  }\n}\n\n// Smooth linear interpolation that accounts for delta time\nfunction damp(a, b, lambda, dt) {\n  return lerp(a, b, 1 - Math.exp(-lambda * dt));\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(0, 0, 0);\n\n  fill(\"white\");\n  noStroke();\n  if (analyserNode) {\n    // Interpolate the previous frame's data to the new frame\n    for (let i = 0; i < analyserData.length; i++) {\n      analyserData[i] = damp(\n        analyserData[i],\n        isFloat ? analyserTarget[i] : (analyserTarget[i] / 256) * 2 - 1,\n        0.01,\n        deltaTime\n      );\n    }\n\n    // draw your scene\n    noFill();\n    stroke(\"white\");\n\n    // draw each sample within the data\n    beginShape();\n    const margin = 0.1;\n\n    for (let i = 0; i < analyserData.length; i++) {\n      // Map sample to screen X position\n      const x = map(\n        i,\n        0,\n        analyserData.length,\n        width * margin,\n        width * (1 - margin)\n      );\n\n      // Signal coming from this frequency bin\n      const signal = analyserData[i];\n\n      // Boost the signal a little so it shows better\n      const amplitude = height * 4;\n\n      // Map signal to screen Y position\n      const y = map(\n        signal,\n        -1,\n        1,\n        height / 2 - amplitude / 2,\n        height / 2 + amplitude / 2\n      );\n\n      // Place vertex\n      vertex(x, y);\n    }\n\n    // Finish the line\n    endShape();\n  } else {\n    // Draw a play button\n    const dim = min(width, height);\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/06-meter.js",
    "content": "let audioContext;\nlet audio;\nlet signalData;\nlet analyserNode;\n\nfunction mousePressed() {\n  if (!audioContext) {\n    // Create a new audio context\n    audioContext = new AudioContext();\n\n    // Create <audio> tag\n    audio = document.createElement(\"audio\");\n\n    // set URL to the MP3 within your Glitch.com assets\n    audio.src = \"audio/piano.mp3\";\n\n    // To play audio through Glitch.com CDN\n    audio.crossOrigin = \"Anonymous\";\n\n    // Enable looping so the audio never stops\n    audio.loop = true;\n\n    // Play audio\n    audio.play();\n\n    // Create a \"Media Element\" source node\n    const source = audioContext.createMediaElementSource(audio);\n\n    // Create an analyser\n    analyserNode = audioContext.createAnalyser();\n    analyserNode.smoothingTimeConstant = 1;\n\n    // Create FFT data\n    signalData = new Float32Array(analyserNode.fftSize);\n\n    // Connect the source to the destination (speakers/headphones)\n    source.connect(audioContext.destination);\n\n    // Connect the source to the analyser node as well\n    source.connect(analyserNode);\n  } else {\n    // Clean up our element and audio context\n    if (audio.paused) audio.play();\n    else audio.pause();\n  }\n}\n\n// Get the root mean squared of a set of signals\nfunction rootMeanSquaredSignal(data) {\n  let rms = 0;\n  for (let i = 0; i < data.length; i++) {\n    rms += data[i] * data[i];\n  }\n  return Math.sqrt(rms / data.length);\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (audioContext) {\n    // Get the *time domain* data (not the frequency)\n    analyserNode.getFloatTimeDomainData(signalData);\n\n    // Get the root mean square of the data\n    const signal = rootMeanSquaredSignal(signalData);\n    const scale = 10; // scale the data a bit so the circle is bigger\n    const size = dim * scale * signal;\n\n    stroke(\"white\");\n    noFill();\n    strokeWeight(dim * 0.0075);\n    circle(width / 2, height / 2, size);\n  } else {\n    fill(\"white\");\n    noStroke();\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/07-meter-levels.js",
    "content": "let audioContext;\nlet audio;\nlet signals;\n\n// Isolate specific bands of frequency with their own colors\nconst frequencyBands = [\n  { frequency: 55, color: \"#D5B3E5\" },\n  { frequency: 110, color: \"#7F3CAC\" },\n  { frequency: 220, color: \"#22A722\" },\n  { frequency: 440, color: \"#F1892A\" },\n  { frequency: 570, color: \"#E84420\" },\n  { frequency: 960, color: \"#F4CD00\" },\n  { frequency: 2000, color: \"#3E58E2\" },\n  { frequency: 4000, color: \"#F391C7\" },\n];\n\nfunction mousePressed() {\n  if (!audioContext) {\n    // Create a new audio context\n    audioContext = new AudioContext();\n\n    // Create <audio> tag\n    audio = document.createElement(\"audio\");\n\n    // set URL to the MP3 within your Glitch.com assets\n    audio.src = \"audio/piano.mp3\";\n\n    // To play audio through Glitch.com CDN\n    audio.crossOrigin = \"Anonymous\";\n\n    // Enable looping so the audio never stops\n    audio.loop = true;\n\n    // Play audio\n    audio.play();\n\n    // Create a \"Media Element\" source node\n    const source = audioContext.createMediaElementSource(audio);\n\n    // Connect the source to the destination (speakers/headphones)\n    source.connect(audioContext.destination);\n\n    // For each frequency we want to isolate, we will create\n    // its own analyser and filter nodes\n    signals = frequencyBands.map(({ frequency, color }) => {\n      // Create an analyser\n      const analyser = audioContext.createAnalyser();\n      analyser.smoothingTimeConstant = 1;\n\n      // Create FFT data\n      const data = new Float32Array(analyser.fftSize);\n\n      // Create a filter that will only allow a band of data\n      // through\n      const filter = audioContext.createBiquadFilter();\n      filter.frequency.value = frequency;\n      filter.Q.value = 1;\n      filter.type = \"bandpass\";\n\n      source.connect(filter);\n      filter.connect(analyser);\n\n      return {\n        analyser,\n        color,\n        data,\n        filter,\n      };\n    });\n  } else {\n    // Clean up our element and audio context\n    if (audio.paused) audio.play();\n    else audio.pause();\n  }\n}\n\n// Get the root mean squared of a set of signals\nfunction rootMeanSquaredSignal(data) {\n  let rms = 0;\n  for (let i = 0; i < data.length; i++) {\n    rms += data[i] * data[i];\n  }\n  return Math.sqrt(rms / data.length);\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (audioContext) {\n    signals.forEach(({ analyser, data, color }, i) => {\n      // Get the waveform\n      analyser.getFloatTimeDomainData(data);\n\n      // Get the root mean square of the data\n      // Note this will already have been 'filtered'\n      // down to the band of frequency we want\n      const signal = rootMeanSquaredSignal(data);\n      const scale = 10; // scale the data a bit so the circle is bigger\n      const size = dim * scale * signal;\n\n      // Draw the rectangle\n      fill(color);\n      noStroke();\n      rectMode(CENTER);\n      const margin = 0.2 * dim;\n      const x =\n        signals.length <= 1\n          ? width / 2\n          : map(i, 0, signals.length - 1, margin, width - margin);\n      const sliceWidth = ((width - margin * 2) / (signals.length - 1)) * 0.75;\n      rect(x, height / 2, sliceWidth, size);\n    });\n  } else {\n    fill(\"white\");\n    noStroke();\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/08-frequency.js",
    "content": "let audioContext;\nlet audio;\nlet analyserNode;\nlet frequencyData;\n\nfunction mousePressed() {\n  if (!audioContext) {\n    // Create a new audio context\n    audioContext = new AudioContext();\n\n    // Create <audio> tag\n    audio = document.createElement(\"audio\");\n\n    // set URL to the MP3 within your Glitch.com assets\n    audio.src = \"audio/piano.mp3\";\n\n    // To play audio through Glitch.com CDN\n    audio.crossOrigin = \"Anonymous\";\n\n    // Enable looping so the audio never stops\n    audio.loop = true;\n\n    // Play audio\n    audio.play();\n\n    // Create a \"Media Element\" source node\n    const source = audioContext.createMediaElementSource(audio);\n\n    analyserNode = audioContext.createAnalyser();\n\n    // Get some higher resolution toward the low end\n    analyserNode.fftSize = 2048 * 2;\n\n    // These are the defaults but different tracks might\n    // need different values\n    analyserNode.minDecibels = -100;\n    analyserNode.maxDecibels = -30;\n\n    frequencyData = new Float32Array(analyserNode.fftSize);\n\n    // Connect source to analyser node\n    source.connect(analyserNode);\n\n    // Connect the source to the destination (speakers/headphones)\n    source.connect(audioContext.destination);\n  } else {\n    // Clean up our element and audio context\n    audio.pause();\n    audioContext.close();\n    audioContext = audio = null;\n  }\n}\n\n// Convert the frequency in Hz to an index in the array\nfunction frequencyToIndex(frequencyHz, sampleRate, frequencyBinCount) {\n  const nyquist = sampleRate / 2;\n  const index = Math.round((frequencyHz / nyquist) * frequencyBinCount);\n  return Math.min(frequencyBinCount, Math.max(0, index));\n}\n\n// Convert an index in a array to a frequency in Hz\nfunction indexToFrequency(index, sampleRate, frequencyBinCount) {\n  return (index * sampleRate) / (frequencyBinCount * 2);\n}\n\n// Get the normalized audio signal (0..1) between two frequencies\nfunction audioSignal(analyser, frequencies, minHz, maxHz) {\n  if (!analyser) return 0;\n  const sampleRate = analyser.context.sampleRate;\n  const binCount = analyser.frequencyBinCount;\n  let start = frequencyToIndex(minHz, sampleRate, binCount);\n  const end = frequencyToIndex(maxHz, sampleRate, binCount);\n  const count = end - start;\n  let sum = 0;\n  for (; start < end; start++) {\n    sum += frequencies[start];\n  }\n\n  const minDb = analyserNode.minDecibels;\n  const maxDb = analyserNode.maxDecibels;\n  const valueDb = count === 0 || !isFinite(sum) ? minDb : sum / count;\n  return map(valueDb, minDb, maxDb, 0, 1, true);\n}\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(\"black\");\n\n  fill(\"white\");\n  noStroke();\n\n  // Draw play/pause button\n  const dim = min(width, height);\n  if (audioContext) {\n    analyserNode.getFloatFrequencyData(frequencyData);\n\n    const cx = width / 2;\n    const cy = height / 2;\n    const radius = dim * 0.75;\n    strokeWeight(dim * 0.0075);\n\n    noFill();\n\n    // draw the low frequency signal\n    stroke(\"#E84420\");\n    const drum = audioSignal(analyserNode, frequencyData, 150, 2500);\n    circle(cx, cy, radius * drum);\n\n    // draw the higher frequency signal\n    stroke(\"#F4CD00\");\n    const voice = audioSignal(analyserNode, frequencyData, 50, 150);\n    circle(cx, cy, radius * voice);\n  } else {\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/09-frequency-advanced.js",
    "content": "/* eslint-disable */\n\nlet audioContext;\nlet frequencyData;\nlet analyserNode;\nlet currentHue = 0;\nlet maxFrequencyTarget = 0;\nlet audio;\n\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n\n  // Optional:\n  // If the user inserts/removes bluetooth headphones or pushes\n  // the play/pause media keys, we can use the following to ignore the action\n  navigator.mediaSession.setActionHandler(\"pause\", () => {});\n}\n\nfunction mousePressed() {\n  // Only initiate audio upon a user gesture\n  if (!audioContext) {\n    audioContext = new AudioContext();\n\n    // Make a stream source, i.e. MP3, microphone, etc\n    // In this case we choose an <audio> element\n    audio = document.createElement(\"audio\");\n\n    // Upon loading the audio, let's play it\n    audio.addEventListener(\n      \"canplay\",\n      () => {\n        // First, ensure the context is in a resumed state\n        audioContext.resume();\n        // Now, play the audio\n        audio.play();\n      },\n      { once: true }\n    );\n\n    // Enable looping\n    audio.loop = true;\n\n    // Set source\n    audio.crossOrigin = \"Anonymous\";\n    audio.src = \"audio/piano.mp3\";\n\n    // Connect source into the WebAudio context\n    const source = audioContext.createMediaElementSource(audio);\n    source.connect(audioContext.destination);\n\n    analyserNode = audioContext.createAnalyser();\n\n    const detail = 4;\n    analyserNode.fftSize = 2048 * detail;\n\n    analyserNode.minDecibels = -100;\n    analyserNode.maxDecibels = -50;\n    frequencyData = new Float32Array(analyserNode.frequencyBinCount);\n\n    source.connect(analyserNode);\n  } else {\n    audio.pause();\n    audioContext.close();\n    audioContext = null;\n  }\n}\n\n// Convert the frequency in Hz to an index in the array\nfunction frequencyToIndex(frequencyHz, sampleRate, frequencyBinCount) {\n  const nyquist = sampleRate / 2;\n  const index = Math.round((frequencyHz / nyquist) * frequencyBinCount);\n  return Math.min(frequencyBinCount, Math.max(0, index));\n}\n\n// Convert an index in a array to a frequency in Hz\nfunction indexToFrequency(index, sampleRate, frequencyBinCount) {\n  return (index * sampleRate) / (frequencyBinCount * 2);\n}\n\n// Get the normalized audio signal (0..1) between two frequencies\nfunction audioSignal(analyser, frequencies, minHz, maxHz) {\n  if (!analyser) return 0;\n  const sampleRate = analyser.context.sampleRate;\n  const binCount = analyser.frequencyBinCount;\n  let start = frequencyToIndex(minHz, sampleRate, binCount);\n  const end = frequencyToIndex(maxHz, sampleRate, binCount);\n  const count = end - start;\n  let sum = 0;\n  for (; start < end; start++) {\n    sum += frequencies[start];\n  }\n\n  const minDb = analyserNode.minDecibels;\n  const maxDb = analyserNode.maxDecibels;\n  const valueDb = count === 0 || !isFinite(sum) ? minDb : sum / count;\n  return map(valueDb, minDb, maxDb, 0, 1, true);\n}\n\n// Find the frequency band that has the most peak signal\nfunction audioMaxFrequency(analyserNode, frequencies) {\n  let maxSignal = -Infinity;\n  let maxSignalIndex = 0;\n  for (let i = 0; i < frequencies.length; i++) {\n    const signal = frequencies[i];\n    if (signal > maxSignal) {\n      maxSignal = signal;\n      maxSignalIndex = i;\n    }\n  }\n  return indexToFrequency(\n    maxSignalIndex,\n    analyserNode.context.sampleRate,\n    analyserNode.frequencyBinCount\n  );\n}\n\nfunction damp(a, b, lambda, dt) {\n  return lerp(a, b, 1 - Math.exp(-lambda * dt));\n}\n\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\nfunction draw() {\n  // fill background\n  background(240);\n  noStroke();\n\n  rectMode(CENTER);\n\n  if (analyserNode) {\n    analyserNode.getFloatFrequencyData(frequencyData);\n    maxFrequencyTarget = map(\n      audioMaxFrequency(analyserNode, frequencyData),\n      0,\n      500,\n      0,\n      360,\n      true\n    );\n  }\n\n  const cx = width / 2;\n  const cy = height / 2;\n  const dim = min(width, height);\n\n  colorMode(HSL);\n  currentHue = damp(currentHue, maxFrequencyTarget, 0.001, deltaTime);\n\n  let hueA = currentHue;\n  let hueB = (hueA + 45) % 360;\n  const colorA = color(hueA, 50, 50);\n  const colorB = color(hueB, 50, 50);\n\n  const maxSize = dim * 0.75;\n  const minSize = dim * 0.15;\n\n  const count = 6;\n\n  background(currentHue, 50, 50);\n\n  for (let i = 0; i < count; i++) {\n    const t = map(i, 0, count - 1, 0, 1);\n    const c = color((currentHue + 90 * ((i + 1) / count)) % 360, 50, 50);\n\n    const minBaseHz = 200;\n    const maxBaseHz = 2000;\n    const minHz = map(count - i, 0, count, minBaseHz, maxBaseHz);\n    const maxHz = map(count - i + 1, 0, count, minBaseHz, maxBaseHz);\n\n    const signal = analyserNode\n      ? audioSignal(analyserNode, frequencyData, minHz, maxHz)\n      : 0;\n\n    const baseSize = map(i, 0, count - 1, maxSize, minSize);\n    const size = baseSize + (maxSize / 4) * signal;\n    const edge = 0.5;\n\n    fill(c);\n    rect(cx, cy + ((maxSize - size) * edge) / 2, size, size);\n  }\n\n  if (!audioContext) {\n    // Draw a play button\n    const dim = min(width, height);\n    fill(\"white\");\n    noStroke();\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/10-tone-demo.js",
    "content": "// Master volume in decibels\nconst volume = -15;\n\n// The synth we'll use for audio\nlet synth;\n\n// Create a new canvas to the browser size\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n\n  // Clear with black on setup\n  background(0);\n\n  // Make the volume quieter\n  Tone.Master.volume.value = volume;\n\n  // Setup a synth with ToneJS\n  synth = new Tone.Synth({\n    oscillator: {\n      type: \"sine\",\n    },\n  });\n\n  // Wire up our nodes:\n  // synth->master\n  synth.connect(Tone.Master);\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  const dim = Math.min(width, height);\n\n  // Black background\n  background(0);\n\n  // Get a 0..1 value for the mouse\n  const u = max(0, min(1, mouseX / width));\n\n  // Choose a frequency that sounds good\n  const frequency = lerp(75, 2500, u);\n  synth.setNote(frequency);\n\n  if (mouseIsPressed) {\n    const time = millis() / 1000;\n\n    const verts = 1000;\n    noFill();\n    stroke(255);\n    strokeWeight(dim * 0.005);\n    beginShape();\n    for (let i = 0; i < verts; i++) {\n      const t = verts <= 1 ? 0.5 : i / (verts - 1);\n      const x = t * width;\n      let y = height / 2;\n\n      // This is not an accurate representation, but\n      // instead exaggerated for the sake of visualization\n      const frequencyMod = lerp(1, 1000, pow(u, 5));\n      const amplitude = sin(time + t * frequencyMod);\n\n      y += (amplitude * height) / 2;\n\n      vertex(x, y);\n    }\n    endShape();\n  }\n\n  // Draw a 'play' button\n  noStroke();\n  fill(255);\n  polygon(width / 2, height / 2, dim * 0.1, 3);\n}\n\n// Update the FX and trigger synth ON\nfunction mousePressed() {\n  synth.triggerAttack();\n}\n\n// Trigger synth OFF\nfunction mouseReleased() {\n  synth.triggerRelease();\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/11-tone-tap.js",
    "content": "// Master volume in decibels\nconst volume = -2;\n\n// The synth we'll use for audio\nlet synth;\n\nlet mouse;\n\n// Create a new canvas to the browser size\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n\n  // Clear with black on setup\n  background(0);\n\n  // Make the volume quieter\n  Tone.Master.volume.value = volume;\n\n  // Setup a synth with ToneJS\n  synth = new Tone.Synth({\n    oscillator: {\n      type: \"sine\",\n    },\n  });\n\n  // Wire up our nodes:\n  // synth->master\n  synth.connect(Tone.Master);\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n\n  // Clear with black on resize\n  background(0);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  const dim = Math.min(width, height);\n\n  // Instead of drawing black, we draw with\n  // transparent black to give a 'ghosting' effect\n  const opacity = 0.085;\n  background(0, 0, 0, opacity * 255);\n\n  // If we have a mouse position, draw it\n  if (mouse) {\n    noFill();\n    stroke(255);\n    strokeWeight(dim * 0.01);\n    circle(mouse[0], mouse[1], dim * 0.2);\n\n    // Clear position so we stop drawing it,\n    // this will make it fade away\n    mouse = null;\n  }\n\n  // Draw a 'play' button\n  noStroke();\n  fill(255);\n  polygon(width / 2, height / 2, dim * 0.1, 3);\n}\n\n// Update mouse position and play a sound\nfunction mousePressed() {\n  // Store mouse position when pressed\n  mouse = [mouseX, mouseY];\n\n  // Hirajoshi scale in C\n  // https://www.pianoscales.org/hirajoshi.html\n  const notes = [\"C\", \"Db\", \"F\", \"Gb\", \"Bb\"];\n  const octaves = [2, 3, 4];\n  const octave = random(octaves);\n  const note = random(notes);\n  synth.triggerAttackRelease(note + octave, \"8n\");\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/12-tone-patatap.js",
    "content": "// Master volume in decibels\nconst volume = -2;\n\n// The synth we'll use for audio\nlet synth;\n\nlet risoColors;\nlet colorJSON;\nlet active = false;\n\nfunction preload() {\n  // loads a JSON as an object\n  colorJSON = loadJSON(\"js/riso-colors.json\");\n}\n\n// Create a new canvas to the browser size\nfunction setup() {\n  createCanvas(windowWidth, windowHeight);\n  background(\"black\");\n\n  // unpack the JSON object as an array\n  risoColors = Object.values(colorJSON);\n\n  // Make the volume quieter\n  Tone.Master.volume.value = volume;\n\n  // Setup a synth with ToneJS\n  synth = new Tone.Synth({\n    oscillator: {\n      type: \"sine\",\n    },\n  });\n\n  // Wire up our nodes:\n  // synth->master\n\n  var feedbackDelay = new Tone.FeedbackDelay(\"8n\", 0.6);\n  synth.connect(feedbackDelay);\n  synth.connect(Tone.Master);\n  feedbackDelay.connect(Tone.Master);\n\n  frameRate(25);\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  // We slowly clear each frame\n  const opacity = 0.05;\n  background(0, 0, 0, opacity * 255);\n\n  if (!active) {\n    const dim = min(width, height);\n    noStroke();\n    fill(255);\n    polygon(width / 2, height / 2, dim * 0.05, 3);\n  }\n}\n\n// Update mouse position and play a sound\nfunction mousePressed() {\n  // First time we click...\n  if (!active) {\n    active = true;\n    // Clear background to white to create an initial flash\n    background(255);\n  }\n\n  // choose a note\n  const note = random([\"A3\", \"C4\", \"D4\", \"E3\", \"G4\"]);\n  synth.triggerAttackRelease(note, \"8n\");\n\n  const dim = min(width, height);\n  const x = mouseX;\n  const y = mouseY;\n\n  noStroke();\n  const curColorData = random(risoColors);\n  const curColor = color(curColorData.hex);\n  const size = max(10, abs(randomGaussian(dim / 8, dim / 8)));\n  const type = random([\"circle\", \"line\", \"polygon\"]);\n  curColor.setAlpha(255 * 0.25);\n  background(curColor);\n  curColor.setAlpha(255);\n\n  fill(curColor);\n  textAlign(CENTER, CENTER);\n  textFont(\"monospace\");\n  text(curColorData.pantone, x, y + size / 2 + 20);\n  text(curColorData.name, x, y - size / 2 - 20);\n  if (type === \"circle\") {\n    ellipseMode(CENTER);\n    circle(x, y, size);\n  } else if (type === \"line\") {\n    strokeWeight(dim * 0.01);\n    stroke(curColor);\n    polygon(x, y, size * 0.5, 2, random(-1, 1) * PI * 2);\n  } else if (type === \"polygon\") {\n    polygon(x, y, size * 0.5, floor(random(3, 10)), random(-1, 1) * PI * 2);\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/13-tone-effects.js",
    "content": "// The setup() function is async\n// So it might take a little while to load\nlet ready = false;\n\n// The synth that plays notes\nlet synth;\n\n// Can be 'sine', 'sawtooth', 'triangle', 'square'\n// Can also add suffixes like sine8, square4\nconst type = \"square\";\n\n// Global volume in decibels\nconst volume = -15;\n\n// The filter and effect nodes which we will modulate\nlet filter, effect;\n\n// Min and max frequency (Hz) cutoff range for the filter\nconst filterMin = 100;\nconst filterMax = 5000;\n\n// 0..1 values for our FX\nlet fxU = 0.5;\nlet fxV = 0.5;\n\n// The notes we will use\nconst notes = [\"C5\", \"A3\", \"D4\", \"G4\", \"A4\", \"F4\"];\n\n// Create a new canvas to the browser size\nasync function setup() {\n  createCanvas(windowWidth, windowHeight);\n\n  // Clear with black on setup\n  background(0);\n\n  // Make the volume quieter\n  Tone.Master.volume.value = volume;\n\n  // Setup a reverb with ToneJS\n  const reverb = new Tone.Reverb({\n    decay: 5,\n    wet: 0.5,\n    preDelay: 0.2,\n  });\n\n  // Load the reverb\n  await reverb.generate();\n\n  // Create an effect node that creates a feedback delay\n  effect = new Tone.FeedbackDelay(0.4, 0.85);\n\n  // Create a new filter for the X slider\n  filter = new Tone.Filter();\n  filter.type = \"lowpass\";\n\n  // Setup a synth with ToneJS\n  synth = new Tone.Synth({\n    oscillator: {\n      // We prefix 'fat' so we can spread the oscillator over multiple frequencies\n      type: `fat${type}`,\n      count: 3,\n      spread: 30,\n    },\n    envelope: {\n      attack: 0.001,\n      decay: 0.1,\n      sustain: 0.5,\n      release: 0.1,\n      attackCurve: \"exponential\",\n    },\n  });\n\n  // Now lets wire up our stack like so:\n  // synth->effect->reverb->filter->master\n  synth.connect(effect);\n  effect.connect(reverb);\n  reverb.connect(filter);\n  filter.connect(Tone.Master);\n\n  // Now we're ready for drawing!\n  ready = true;\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n\n  // Clear to black on resize\n  background(0);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  // Make sure async setup() is done before we draw\n  if (!ready) return;\n\n  filter.frequency.value = lerp(filterMin, filterMax, fxU);\n  effect.wet.value = fxV;\n\n  // For consistent sizing regardless of portrait/landscape\n  const dim = Math.min(width, height);\n\n  // Black background\n  background(0, 0, 0, 20);\n\n  // draw the two FX knobs\n  if (mouseIsPressed) {\n    noFill();\n    strokeWeight(dim * 0.0175);\n    stroke(255);\n    drawEffectKnob(dim * 0.4, fxU);\n    drawEffectKnob(dim * 0.6, fxV);\n  }\n\n  // Draw a 'play' button\n  noStroke();\n  fill(255);\n  polygon(width / 2, height / 2, dim * 0.1, 3);\n}\n\n// Draws an arc with the given amount of 'strength'\nfunction drawEffectKnob(radius, t) {\n  if (t <= 0) return;\n  arc(width / 2, height / 2, radius, radius, 0, PI * 2 * t);\n}\n\n// Update FX values based on mouse position\nfunction updateEffects() {\n  fxU = max(0, min(1, mouseX / width));\n  fxV = max(0, min(1, mouseY / height));\n}\n\n// Update the FX and trigger synth ON\nfunction mousePressed() {\n  updateEffects();\n  if (synth) synth.triggerAttack(random(notes));\n}\n\n// Update the FX values\nfunction mouseDragged() {\n  updateEffects();\n}\n\n// Trigger synth OFF\nfunction mouseReleased() {\n  if (synth) synth.triggerRelease();\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/14-tone-sequencer.js",
    "content": "// Master volume in decior audio\nlet synth;\n\n// Whether the audio sequence is playing\nlet playing = false;\n\n// The current Tone.Sequence\nlet sequence;\n\n// The currently playing column\nlet currentColumn = 0;\n\n// Here is the fixed scale we will use\nconst notes = [\"A3\", \"C4\", \"D4\", \"E3\", \"G4\"];\n\n// Also can try other scales/notes\n// const notes = [\"F#4\", \"E4\", \"C#4\", \"A4\"];\n// const notes = ['A3', 'C4', 'D4', 'E4', 'G4', 'A4'];\n// const notes = [ \"A4\", \"D3\", \"E3\", \"G4\", 'F#4' ];\n\n// Number of rows is the number of different notes\nconst numRows = notes.length;\n\n// Number of columns is depending on how many notes to play in a measure\nconst numCols = 16;\nconst noteInterval = `${numCols}n`;\n\n// Setup audio config\nTone.Transport.bpm.value = 120;\n\n// Create a Row*Col data structure that has nested arrays\n// [ [ 0, 0, 0 ], [ 0, 0, 0 ], ... ]\n// The data can be 0 (off) or 1 (on)\nconst data = [];\nfor (let y = 0; y < numRows; y++) {\n  const row = [];\n  for (let x = 0; x < numCols; x++) {\n    row.push(0);\n  }\n  data.push(row);\n}\n\n// Create a new canvas to the browser size\nasync function setup() {\n  // Setup canvas size as a square\n  const dim = min(windowWidth, windowHeight);\n  createCanvas(innerWidth, innerHeight);\n\n  // Clear with black on setup\n  background(0);\n\n  // Setup a reverb with ToneJS\n  const reverb = new Tone.Reverb({\n    decay: 4,\n    wet: 0.2,\n    preDelay: 0.25,\n  });\n\n  // Load the reverb\n  await reverb.generate();\n\n  // Create an effect node that creates a feedback delay\n  const effect = new Tone.FeedbackDelay(`${Math.floor(numCols / 2)}n`, 1 / 3);\n  effect.wet.value = 0.2;\n\n  // Setup a synth with ToneJS\n  // We use a poly synth which can hold up to numRows voices\n  // Then we will play each note on a different voice\n  synth = new Tone.PolySynth(numRows, Tone.DuoSynth);\n\n  // Setup the synths a little bit\n  synth.set({\n    voice0: {\n      oscillator: {\n        type: \"triangle4\",\n      },\n      volume: -30,\n      envelope: {\n        attack: 0.005,\n        release: 0.05,\n        sustain: 1,\n      },\n    },\n    voice1: {\n      volume: -10,\n      envelope: {\n        attack: 0.005,\n        release: 0.05,\n        sustain: 1,\n      },\n    },\n  });\n  synth.volume.value = -10;\n\n  // Wire up our nodes:\n  synth.connect(effect);\n  synth.connect(Tone.Master);\n  effect.connect(reverb);\n  reverb.connect(Tone.Master);\n\n  // Every two measures, we randomize the notes\n  // We use Transport to schedule timer since it has\n  // to be exactly in sync with the audio\n  Tone.Transport.scheduleRepeat(() => {\n    randomizeSequencer();\n  }, \"2m\");\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  // const dim = max(windowWidth, windowHeight);\n  resizeCanvas(innerWidth, innerHeight);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  // Our synth isn't loaded yet, don't draw anything\n  if (!synth) return;\n\n  const dim = min(width, height);\n\n  // Black background\n  background(0);\n\n  if (playing) {\n    // The audio is playing so we can show the sequencer\n    const margin = dim * 0.2;\n    const innerSize = dim - margin * 2;\n    const cellSize = innerSize / numCols;\n    push();\n    translate(innerWidth / 2 - dim / 2, innerHeight / 2 - dim / 2);\n    // Loop through the nested data structure, drawing each note\n    for (let y = 0; y < data.length; y++) {\n      const row = data[y];\n      for (let x = 0; x < row.length; x++) {\n        const u = x / (numCols - 1);\n        const v = y / (numRows - 1);\n        let px = lerp(margin, dim - margin, u);\n        let py = lerp(margin, dim - margin, v);\n\n        noStroke();\n        noFill();\n\n        // note on=fill, note off=stroke\n        if (row[x] === 1) fill(255);\n        else stroke(255);\n\n        // draw note\n        circle(px, py, cellSize / 2);\n\n        // draw a rectangle around the currently playing column\n        if (x === currentColumn) {\n          rectMode(CENTER);\n          rect(px, py, cellSize, cellSize);\n        }\n      }\n    }\n    pop();\n  } else {\n    // Draw a 'play' button\n    noStroke();\n    fill(255);\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Here we randomize the sequencer with some data\nfunction randomizeSequencer() {\n  // Choose a % chance so that sometimes it is more busy, other times more sparse\n  const chance = random(0.5, 1.5);\n  for (let y = 0; y < data.length; y++) {\n    // Loop through and create some random on/off values\n    const row = data[y];\n    for (let x = 0; x < row.length; x++) {\n      row[x] = randomGaussian() > chance ? 1 : 0;\n    }\n    // Loop through again and make sure we don't have two\n    // consectutive on values (it sounds bad)\n    for (let x = 0; x < row.length - 1; x++) {\n      if (row[x] === 1 && row[x + 1] === 1) {\n        row[x + 1] = 0;\n        x++;\n      }\n    }\n  }\n}\n\n// When the mouse is pressed, turn on the sequencer\nfunction mousePressed() {\n  // No synth loaded yet, just skip mouse click\n  if (!synth) {\n    return;\n  }\n\n  if (playing) {\n    // If we are currently playing, we stop the sequencer\n    playing = false;\n    sequence.stop();\n    Tone.Transport.stop();\n  } else {\n    // If we aren't currently playing, we can start the sequence\n\n    // We do this by creating an array of indices [ 0, 1, 2 ... 15 ]\n    const noteIndices = newArray(numCols);\n    // create the sequence, passing onSequenceStep function\n    sequence = new Tone.Sequence(onSequenceStep, noteIndices, noteInterval);\n\n    // Start the sequence and Transport loop\n    playing = true;\n    sequence.start();\n    Tone.Transport.start();\n  }\n}\n\n// Here is where we actually play the audi\nfunction onSequenceStep(time, column) {\n  // We build up a list of notes, which will equal\n  // the numRows. This gets passed into our PolySynth\n  let notesToPlay = [];\n\n  // Go through each row\n  data.forEach((row, rowIndex) => {\n    // See if the note is \"on\"\n    const isOn = row[column] == 1;\n    // If its on, add it to the list of notes to play\n    if (isOn) {\n      const note = notes[rowIndex];\n      notesToPlay.push(note);\n    }\n  });\n\n  // Trigger a note\n  const velocity = random(0.5, 1);\n  synth.triggerAttackRelease(notesToPlay, noteInterval, time, velocity);\n  Tone.Draw.schedule(function () {\n    currentColumn = column;\n  }, time);\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n\n// A utility function to create a new array\n// full of indices [ 0, 1, 2, ... (N - 1) ]\nfunction newArray(n) {\n  const array = [];\n  for (let i = 0; i < n; i++) {\n    array.push(i);\n  }\n  return array;\n}\n"
  },
  {
    "path": "src/js/15-tone-mp3-effects.js",
    "content": "// Master volume in decibels\nconst volume = -16;\n\nconst MP3 = \"audio/piano.mp3\";\n\n// The synth we'll use for audio\nlet player;\n\nlet autoFilter;\n\n// Create a new canvas to the browser size\nasync function setup() {\n  createCanvas(windowWidth, windowHeight);\n\n  // Make the volume quieter\n  Tone.Master.volume.value = volume;\n\n  // We can use 'player' to play MP3 files\n  player = new Tone.Player();\n  player.loop = true;\n  player.autostart = false;\n  player.loopStart = 1.0;\n\n  // Load and \"await\" the MP3 file\n  await player.load(MP3);\n\n  // Wire up connections\n  autoFilter = new Tone.AutoFilter(\"8n\");\n  autoFilter.start();\n\n  player.connect(autoFilter);\n  autoFilter.connect(Tone.Master);\n}\n\n// On window resize, update the canvas size\nfunction windowResized() {\n  resizeCanvas(windowWidth, windowHeight);\n}\n\n// Render loop that draws shapes with p5\nfunction draw() {\n  if (!player || !player.loaded) {\n    // MP3 not loaded\n    return;\n  }\n  const dim = Math.min(width, height);\n\n  // Black background\n  background(0);\n\n  autoFilter.wet.value = mouseY / height;\n  autoFilter.frequency.value = map(mouseX, 0, width, 0.5, 1.5);\n\n  // Draw a 'play' or 'stop' button\n  if (player.state === \"started\") {\n    noStroke();\n    fill(255);\n    polygon(width / 2, height / 2, dim * 0.1, 4, PI / 4);\n\n    stroke(\"tomato\");\n    noFill();\n    strokeWeight(dim * 0.0175);\n    circle(mouseX, mouseY, dim * 0.2);\n  } else {\n    noStroke();\n    fill(255);\n    polygon(width / 2, height / 2, dim * 0.1, 3);\n  }\n}\n\n// Update the FX and trigger synth ON\nfunction mousePressed() {\n  if (player && player.loaded) {\n    if (player.state === \"started\") {\n      player.stop();\n    } else {\n      player.start();\n    }\n  }\n}\n\n// Draw a basic polygon, handles triangles, squares, pentagons, etc\nfunction polygon(x, y, radius, sides = 3, angle = 0) {\n  beginShape();\n  for (let i = 0; i < sides; i++) {\n    const a = angle + TWO_PI * (i / sides);\n    let sx = x + cos(a) * radius;\n    let sy = y + sin(a) * radius;\n    vertex(sx, sy);\n  }\n  endShape(CLOSE);\n}\n"
  },
  {
    "path": "src/js/riso-colors.json",
    "content": "[\n  {\n    \"name\": \"Black\",\n    \"hex\": \"#000000\",\n    \"pantone\": \"BLACK U\"\n  },\n  {\n    \"name\": \"Burgundy\",\n    \"hex\": \"#914e72\",\n    \"pantone\": \"235 U\",\n    \"zType\": \"S-4225\"\n  },\n  {\n    \"name\": \"Blue\",\n    \"hex\": \"#0078bf\",\n    \"pantone\": \"3005 U\",\n    \"zType\": \"S-4257\"\n  },\n  {\n    \"name\": \"Green\",\n    \"hex\": \"#00a95c\",\n    \"pantone\": \"354 U\",\n    \"zType\": \"S-4259\"\n  },\n  {\n    \"name\": \"Medium Blue\",\n    \"hex\": \"#3255a4\",\n    \"pantone\": \"286 U\",\n    \"zType\": \"S-4261\"\n  },\n  {\n    \"name\": \"Bright Red\",\n    \"hex\": \"#f15060\",\n    \"pantone\": \"185 U\",\n    \"zType\": \"S-4263\"\n  },\n  {\n    \"name\": \"RisoFederal Blue\",\n    \"hex\": \"#3d5588\",\n    \"pantone\": \"288 U\",\n    \"zType\": \"S-4265\"\n  },\n  {\n    \"name\": \"Purple\",\n    \"hex\": \"#765ba7\",\n    \"pantone\": \"2685 U\",\n    \"zType\": \"S-4267\"\n  },\n  {\n    \"name\": \"Teal\",\n    \"hex\": \"#00838a\",\n    \"pantone\": \"321 U\",\n    \"zType\": \"S-4269\"\n  },\n  {\n    \"name\": \"Flat Gold\",\n    \"hex\": \"#bb8b41\",\n    \"pantone\": \"1245 U\",\n    \"zType\": \"S-4271\"\n  },\n  {\n    \"name\": \"Hunter Green\",\n    \"hex\": \"#407060\",\n    \"pantone\": \"342 U\",\n    \"zType\": \"S-4273\"\n  },\n  {\n    \"name\": \"Red\",\n    \"hex\": \"#ff665e\",\n    \"pantone\": \"WARM RED U\",\n    \"zType\": \"S-4275\"\n  },\n  {\n    \"name\": \"Brown\",\n    \"hex\": \"#925f52\",\n    \"pantone\": \"7526 U\",\n    \"zType\": \"S-4277\"\n  },\n  {\n    \"name\": \"Yellow\",\n    \"hex\": \"#ffe800\",\n    \"pantone\": \"YELLOW U\",\n    \"zType\": \"S-4279\"\n  },\n  {\n    \"name\": \"Marine Red\",\n    \"hex\": \"#d2515e\",\n    \"pantone\": \"186 U\",\n    \"zType\": \"S-4281\"\n  },\n  {\n    \"name\": \"Orange\",\n    \"hex\": \"#ff6c2f\",\n    \"pantone\": \"ORANGE 021 U\",\n    \"zType\": \"S-4283\"\n  },\n  {\n    \"name\": \"Fluorescent Pink\",\n    \"hex\": \"#ff48b0\",\n    \"pantone\": \"806 U\",\n    \"zType\": \"S-4287\"\n  },\n  {\n    \"name\": \"Light Gray\",\n    \"hex\": \"#88898a\",\n    \"pantone\": \"424 U\",\n    \"zType\": \"S-4291\"\n  },\n  {\n    \"name\": \"Metallic Gold\",\n    \"hex\": \"#ac936e\",\n    \"pantone\": \"872 U\",\n    \"zType\": \" S-2772\"\n  },\n  {\n    \"name\": \"Crimson\",\n    \"hex\": \"#e45d50\",\n    \"pantone\": \"485 U\",\n    \"zType\": \"S-4285\"\n  },\n  {\n    \"name\": \"Fluorescent Orange\",\n    \"hex\": \"#ff7477\",\n    \"pantone\": \"805 U\",\n    \"zType\": \"S-4289\"\n  },\n  {\n    \"name\": \"Cornflower\",\n    \"hex\": \"#62a8e5\",\n    \"pantone\": \"292 U\",\n    \"zType\": \"S-4617\"\n  },\n  {\n    \"name\": \"Sky Blue\",\n    \"hex\": \"#4982cf\",\n    \"pantone\": \"285U\",\n    \"zType\": \"S-4618\"\n  },\n  {\n    \"name\": \"Sea Blue\",\n    \"hex\": \"#0074a2\",\n    \"pantone\": \"307 U\",\n    \"zType\": \"S-4619\"\n  },\n  {\n    \"name\": \"Lake\",\n    \"hex\": \"#235ba8\",\n    \"pantone\": \"293 U\",\n    \"zType\": \"S-4620\"\n  },\n  {\n    \"name\": \"Indigo\",\n    \"hex\": \"#484d7a\",\n    \"pantone\": \"2758 U\",\n    \"zType\": \"S-4621\"\n  },\n  {\n    \"name\": \"Midnight\",\n    \"hex\": \"#435060\",\n    \"pantone\": \"296 U\",\n    \"zType\": \"S-4622\"\n  },\n  {\n    \"name\": \"Mist\",\n    \"hex\": \"#d5e4c0\",\n    \"pantone\": \"7485 U\",\n    \"zType\": \"S-4623\"\n  },\n  {\n    \"name\": \"Granite\",\n    \"hex\": \"#a5aaa8\",\n    \"pantone\": \"7538 U\",\n    \"zType\": \"S-4624\"\n  },\n  {\n    \"name\": \"Charcoal\",\n    \"hex\": \"#70747c\",\n    \"pantone\": \"7540 U\",\n    \"zType\": \"S-4625\"\n  },\n  {\n    \"name\": \"Smoky Teal\",\n    \"hex\": \"#5f8289\",\n    \"pantone\": \"5483 U\",\n    \"zType\": \"S-4626\"\n  },\n  {\n    \"name\": \"Steel\",\n    \"hex\": \"#375e77\",\n    \"pantone\": \"302 U\",\n    \"zType\": \"S-4627\"\n  },\n  {\n    \"name\": \"Slate\",\n    \"hex\": \"#5e695e\",\n    \"pantone\": \"5605 U\",\n    \"zType\": \"S-4628\"\n  },\n  {\n    \"name\": \"Turquoise\",\n    \"hex\": \"#00aa93\",\n    \"pantone\": \"3275 U\",\n    \"zType\": \"S-4629\"\n  },\n  {\n    \"name\": \"Emerald\",\n    \"hex\": \"#19975d\",\n    \"pantone\": \"355 U\",\n    \"zType\": \"S-4630\"\n  },\n  {\n    \"name\": \"Grass\",\n    \"hex\": \"#397e58\",\n    \"pantone\": \"356 U\",\n    \"zType\": \"S-4631\"\n  },\n  {\n    \"name\": \"Forest\",\n    \"hex\": \"#516e5a\",\n    \"pantone\": \"357 U\",\n    \"zType\": \"S-4632\"\n  },\n  {\n    \"name\": \"Spruce\",\n    \"hex\": \"#4a635d\",\n    \"pantone\": \"567 U\",\n    \"zType\": \"S-4633\"\n  },\n  {\n    \"name\": \"Moss\",\n    \"hex\": \"#68724d\",\n    \"pantone\": \"371 U\",\n    \"zType\": \"S-4634\"\n  },\n  {\n    \"name\": \"Sea Foam\",\n    \"hex\": \"#62c2b1\",\n    \"pantone\": \"570 U\",\n    \"zType\": \"S-4635\"\n  },\n  {\n    \"name\": \"Kelly Green\",\n    \"hex\": \"#67b346\",\n    \"pantone\": \" 368 U\",\n    \"zType\": \"S-4636\"\n  },\n  {\n    \"name\": \"Light Teal\",\n    \"hex\": \"#009da5\",\n    \"pantone\": \"320 U\",\n    \"zType\": \"S-4637\"\n  },\n  {\n    \"name\": \"Ivy\",\n    \"hex\": \"#169b62\",\n    \"pantone\": \"347 U\",\n    \"zType\": \"S-4638\"\n  },\n  {\n    \"name\": \"Pine\",\n    \"hex\": \"#237e74\",\n    \"pantone\": \"3295 U\",\n    \"zType\": \"S-4639\"\n  },\n  {\n    \"name\": \"Lagoon\",\n    \"hex\": \"#2f6165\",\n    \"pantone\": \"323 U\",\n    \"zType\": \"S-4640\"\n  },\n  {\n    \"name\": \"Violet\",\n    \"hex\": \"#9d7ad2\",\n    \"pantone\": \"265 U\",\n    \"zType\": \"S-4641\"\n  },\n  {\n    \"name\": \"Orchid\",\n    \"hex\": \"#aa60bf\",\n    \"pantone\": \"2592 U\",\n    \"zType\": \"S-4642\"\n  },\n  {\n    \"name\": \"Plum\",\n    \"hex\": \"#845991\",\n    \"pantone\": \"2603 U\",\n    \"zType\": \"S-4644\"\n  },\n  {\n    \"name\": \"Raisin\",\n    \"hex\": \"#775d7a\",\n    \"pantone\": \"519 U\",\n    \"zType\": \"S-4645\"\n  },\n  {\n    \"name\": \"Grape\",\n    \"hex\": \"#6c5d80\",\n    \"pantone\": \"2695 U\",\n    \"zType\": \"S-4646\"\n  },\n  {\n    \"name\": \"Scarlet\",\n    \"hex\": \"#f65058\",\n    \"pantone\": \"RED 032 U\",\n    \"zType\": \"S-4647\"\n  },\n  {\n    \"name\": \"Tomato\",\n    \"hex\": \"#d2515e\",\n    \"pantone\": \"186 U\",\n    \"zType\": \"S-4648\"\n  },\n  {\n    \"name\": \"Cranberry\",\n    \"hex\": \"#d1517a\",\n    \"pantone\": \"214 U\",\n    \"zType\": \"S-4649\"\n  },\n  {\n    \"name\": \"Maroon\",\n    \"hex\": \"#9e4c6e\",\n    \"pantone\": \"221 U\",\n    \"zType\": \"S-4650\"\n  },\n  {\n    \"name\": \"Raspberry Red\",\n    \"hex\": \"#d1517a\",\n    \"pantone\": \"214U\",\n    \"zType\": \"S-4651\"\n  },\n  {\n    \"name\": \"Brick\",\n    \"hex\": \"#a75154\",\n    \"pantone\": \"1807 U\",\n    \"zType\": \"S-4652\"\n  },\n  {\n    \"name\": \"Light Lime\",\n    \"hex\": \"#e3ed55\",\n    \"pantone\": \"387 U\",\n    \"zType\": \"S-4653\"\n  },\n  {\n    \"name\": \"Sunflower\",\n    \"hex\": \"#ffb511\",\n    \"pantone\": \"116 U\",\n    \"zType\": \"S-4654\"\n  },\n  {\n    \"name\": \"Melon\",\n    \"hex\": \"#ffae3b\",\n    \"pantone\": \"1235 U\",\n    \"zType\": \"S-4655\"\n  },\n  {\n    \"name\": \"Apricot\",\n    \"hex\": \"#f6a04d\",\n    \"pantone\": \"143 U\",\n    \"zType\": \"S-4656\"\n  },\n  {\n    \"name\": \"Paprika\",\n    \"hex\": \"#ee7f4b\",\n    \"pantone\": \"158 U\",\n    \"zType\": \"S-4657\"\n  },\n  {\n    \"name\": \"Pumpkin\",\n    \"hex\": \"#ff6f4c\",\n    \"pantone\": \"1655 U\",\n    \"zType\": \"S-4658\"\n  },\n  {\n    \"name\": \"Bright Olive Green\",\n    \"hex\": \"#b49f29\",\n    \"pantone\": \"103 U\",\n    \"zType\": \"S-4659\"\n  },\n  {\n    \"name\": \"Bright Gold\",\n    \"hex\": \"#ba8032\",\n    \"pantone\": \"131 U\",\n    \"zType\": \"S-4660\"\n  },\n  {\n    \"name\": \"Copper\",\n    \"hex\": \"#bd6439\",\n    \"pantone\": \"1525 U\",\n    \"zType\": \"S-4661\"\n  },\n  {\n    \"name\": \"Mahogany\",\n    \"hex\": \"#8e595a\",\n    \"pantone\": \"491 U\",\n    \"zType\": \"S-4662\"\n  },\n  {\n    \"name\": \"Bisque\",\n    \"hex\": \"#f2cdcf\",\n    \"pantone\": \"503 U\",\n    \"zType\": \"S-4663\"\n  },\n  {\n    \"name\": \"Bubble Gum\",\n    \"hex\": \"#f984ca\",\n    \"pantone\": \"231 U\",\n    \"zType\": \"S-4664\"\n  },\n  {\n    \"name\": \"Light Mauve\",\n    \"hex\": \"#e6b5c9\",\n    \"pantone\": \"7430 U\",\n    \"zType\": \"S-4665\"\n  },\n  {\n    \"name\": \"Dark Mauve\",\n    \"hex\": \"#bd8ca6\",\n    \"pantone\": \"687 U\",\n    \"zType\": \"S-4666\"\n  },\n  {\n    \"name\": \"Wine\",\n    \"hex\": \"#914e72\",\n    \"pantone\": \"235 U\",\n    \"zType\": \"S-4674\"\n  },\n  {\n    \"name\": \"Gray\",\n    \"hex\": \"#928d88\",\n    \"pantone\": \"403 U\",\n    \"zType\": \"S-4693\"\n  },\n  {\n    \"name\": \"White\",\n    \"hex\": \"#ffffff\",\n    \"zType\": \"S-4722 \"\n  },\n  {\n    \"name\": \"Aqua\",\n    \"hex\": \"#5ec8e5\",\n    \"pantone\": \"637 U\",\n    \"zType\": \"S-4917\"\n  },\n  {\n    \"name\": \"Mint\",\n    \"hex\": \"#82d8d5\",\n    \"pantone\": \"324 U\",\n    \"zType\": \"S-6316\"\n  },\n  {\n    \"name\": \"Fluorescent Yellow\",\n    \"hex\": \"#ffe900\",\n    \"pantone\": \"803 U\",\n    \"zType\": \"S-7761\"\n  },\n  {\n    \"name\": \"Fluorescent Red\",\n    \"hex\": \"#ff4c65\",\n    \"pantone\": \"812 U\",\n    \"zType\": \"S-7762\"\n  },\n  {\n    \"name\": \"Fluorescent Green\",\n    \"hex\": \"#44d62c\",\n    \"pantone\": \"802 U\",\n    \"zType\": \"S-7763\"\n  }\n]\n"
  }
]