| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>mpckit main screen</title>
- <script src="./support.js"></script>
- </head>
- <body>
- <x-dc>
- <helmet>
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
- <style>
- body{margin:0;background:#15171A}
- *{box-sizing:border-box}
- a{color:#F5A524}a:hover{color:#FFC35A}
- </style>
- </helmet>
- <div style="max-width: 1280px; margin: 0 auto; padding: 24px; display: flex; flex-direction: column; gap: 20px; background: #15171A; color: #F2F3F5; font-family: Inter, system-ui, sans-serif; font-size: 14px; line-height: 1.4">
- <div style="display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid #353A43">
- <h1 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-weight: 700; font-size: 24px; letter-spacing: 0.02em">mpckit</h1>
- <button type="button" onClick="{{openAbout}}" style="min-height: 44px; padding: 0 8px; background: transparent; color: {{accent}}; border: 0; font: inherit; font-weight: 500; text-decoration: underline; text-underline-offset: 3px; cursor: pointer">About</button>
- </div>
- <header style="display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px">
- <div style="display: flex; flex-direction: column; gap: 6px; flex: 1 1 240px; min-width: 0; max-width: 360px">
- <label for="project-name" style="font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Project name</label>
- <input id="project-name" type="text" value="{{name}}" onChange="{{setName}}" style="height: 44px; padding: 0 12px; background: #1E2126; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 600; width: 100%">
- </div>
- <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto">
- <button type="button" style="height: 44px; padding: 0 16px; background: #1E2126; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">New</button>
- <button type="button" style="height: 44px; padding: 0 16px; background: #1E2126; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Load</button>
- <button type="button" style="height: 44px; padding: 0 16px; background: #1E2126; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Export zip</button>
- <button type="button" style="height: 44px; padding: 0 16px; background: {{accent}}; color: #15171A; border: 1px solid {{accent}}; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">Export to folder</button>
- </div>
- </header>
- <div role="tablist" aria-label="Views" style="display: flex; gap: 4px; border-bottom: 1px solid #353A43">
- <button type="button" role="tab" aria-selected="{{padsTab.selected}}" onClick="{{padsTab.pick}}" style="height: 48px; padding: 0 22px; background: transparent; color: {{padsTab.fg}}; border: 0; border-bottom: 3px solid {{padsTab.line}}; font-family: 'Space Grotesk', sans-serif; font-size: 16px; font-weight: 700; cursor: pointer">Pads</button>
- <button type="button" role="tab" aria-selected="{{seqsTab.selected}}" onClick="{{seqsTab.pick}}" style="height: 48px; padding: 0 22px; background: transparent; color: {{seqsTab.fg}}; border: 0; border-bottom: 3px solid {{seqsTab.line}}; font-family: 'Space Grotesk', sans-serif; font-size: 16px; font-weight: 700; cursor: pointer">Sequences</button>
- </div>
- <sc-if value="{{showPads}}" hint-placeholder-val="{{ true }}">
- <main style="display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start">
- <section aria-label="Pads" style="flex: 1.4 1 380px; min-width: 0; background: #1E2126; border: 1px solid #353A43; border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 14px">
- <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
- <h2 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Pad bank</h2>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">{{bankSummary}}</span>
- </div>
- <div style="display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px">
- <sc-for list="{{banks}}" as="b" hint-placeholder-count="8">
- <button type="button" onClick="{{b.pick}}" aria-pressed="{{b.pressed}}" aria-label="{{b.aria}}" style="height: 44px; padding: 0; background: {{b.bg}}; color: {{b.fg}}; border: 1px solid {{b.border}}; border-radius: 6px; font-family: 'Space Grotesk', sans-serif; font-size: 15px; font-weight: 700; cursor: pointer">{{b.label}}</button>
- </sc-for>
- </div>
- <div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px">
- <sc-for list="{{cells}}" as="pad" hint-placeholder-count="16">
- <button type="button" onClick="{{pad.pick}}" aria-pressed="{{pad.pressed}}" aria-label="{{pad.aria}}" style="aspect-ratio: 1 / 1; min-height: 72px; padding: 8px; display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; gap: 4px; text-align: left; background: {{pad.bg}}; color: #F2F3F5; border: 2px solid {{pad.border}}; border-radius: 8px; font: inherit; cursor: pointer; overflow: hidden">
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: {{pad.numColor}}">{{pad.label}}</span>
- <span style="font-size: 12px; font-weight: 600; line-height: 1.25; overflow: hidden; max-width: 100%; overflow-wrap: anywhere">{{pad.name}}</span>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #C9CDD4; min-height: 15px">{{pad.badges}}</span>
- </button>
- </sc-for>
- </div>
- <p style="margin: 0; font-size: 12px; color: #A9AEB8">Click a pad to select and hear it. Drag a pad to move it, drag onto a filled pad to swap, hold Alt while dragging to copy. Drop a WAV file on any pad to assign it.</p>
- </section>
- <section aria-label="Pad settings" style="flex: 1 1 320px; min-width: 0; background: #1E2126; border: 1px solid #353A43; border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 16px">
- <div style="display: flex; align-items: center; gap: 12px">
- <div style="font-family: 'Space Grotesk', sans-serif; font-size: 28px; font-weight: 700; color: {{accent}}">{{sel.id}}</div>
- <div style="min-width: 0; display: flex; flex-direction: column; gap: 2px">
- <h2 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Pad settings</h2>
- <div style="font-weight: 600; overflow-wrap: anywhere">{{sel.file}}</div>
- </div>
- </div>
- <sc-if value="{{sel.empty}}" hint-placeholder-val="{{ false }}">
- <div style="display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px dashed #4A505B; border-radius: 8px">
- <p style="margin: 0; color: #C9CDD4">This pad is empty. Drop a WAV file on it, or choose one.</p>
- <button type="button" style="height: 44px; padding: 0 16px; align-self: flex-start; background: {{accent}}; color: #15171A; border: 1px solid {{accent}}; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">Choose WAV file</button>
- </div>
- </sc-if>
- <sc-if value="{{sel.has}}" hint-placeholder-val="{{ true }}">
- <div style="display: flex; flex-direction: column; gap: 16px">
- <div style="display: flex; flex-wrap: wrap; gap: 8px">
- <button type="button" style="height: 44px; padding: 0 16px; display: flex; align-items: center; gap: 8px; background: {{accent}}; color: #15171A; border: 1px solid {{accent}}; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">
- <svg aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M3 2 L12 7 L3 12 Z"></path></svg>
- Play
- </button>
- <button type="button" style="height: 44px; padding: 0 16px; background: #2A2D33; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Replace</button>
- <button type="button" onClick="{{clearPad}}" style="height: 44px; padding: 0 16px; background: #2A2D33; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Clear</button>
- </div>
- <div style="display: flex; flex-direction: column; gap: 8px">
- <h3 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Tuning</h3>
- <div style="display: flex; flex-direction: column; gap: 4px">
- <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
- <label for="tune-coarse" style="font-size: 12px; color: #C9CDD4">Semitones</label>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 500; color: #F2F3F5">{{sel.coarseText}}</span>
- </div>
- <input id="tune-coarse" type="range" min="-24" max="24" step="1" value="{{sel.coarse}}" onChange="{{setCoarse}}" style="width: 100%; height: 44px; margin: 0; accent-color: {{accent}}; cursor: pointer">
- <div aria-hidden="true" style="display: flex; justify-content: space-between; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #A9AEB8">
- <span>-24</span>
- <span>0</span>
- <span>+24</span>
- </div>
- </div>
- <div style="display: flex; flex-direction: column; gap: 4px">
- <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
- <label for="tune-fine" style="font-size: 12px; color: #C9CDD4">Cents</label>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 500; color: #F2F3F5">{{sel.fineText}}</span>
- </div>
- <input id="tune-fine" type="range" min="-90" max="90" step="1" value="{{sel.fine}}" onChange="{{setFine}}" style="width: 100%; height: 44px; margin: 0; accent-color: {{accent}}; cursor: pointer">
- <div aria-hidden="true" style="display: flex; justify-content: space-between; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #A9AEB8">
- <span>-90</span>
- <span>0</span>
- <span>+90</span>
- </div>
- </div>
- </div>
- <div style="display: flex; flex-direction: column; gap: 4px">
- <label for="mute-group" style="font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Mute group</label>
- <select id="mute-group" value="{{sel.mute}}" onChange="{{setMute}}" style="height: 44px; padding: 0 10px; width: 100%; background: #15171A; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit">
- <option value="0">Off</option>
- <option value="1">Group 1</option>
- <option value="2">Group 2</option>
- <option value="3">Group 3</option>
- <option value="4">Group 4</option>
- </select>
- <span style="font-size: 12px; color: #A9AEB8">Pads in the same group cut each other off on the device.</span>
- </div>
- <div style="display: flex; flex-wrap: wrap; gap: 8px; padding-top: 12px; border-top: 1px solid #353A43">
- <button type="button" style="height: 44px; padding: 0 16px; background: #2A2D33; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Move to pad</button>
- <button type="button" style="height: 44px; padding: 0 16px; background: #2A2D33; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Copy to pad</button>
- </div>
- </div>
- </sc-if>
- </section>
- </main>
- </sc-if>
- <sc-if value="{{showSeqs}}" hint-placeholder-val="{{ false }}">
- <main style="display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start">
- <section aria-label="Sequence slots" style="flex: 1.4 1 380px; min-width: 0; background: #1E2126; border: 1px solid #353A43; border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 14px">
- <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
- <h2 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Sequence bank</h2>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">{{seqBankSummary}}</span>
- </div>
- <div style="display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px">
- <sc-for list="{{seqBanks}}" as="b" hint-placeholder-count="8">
- <button type="button" onClick="{{b.pick}}" aria-pressed="{{b.pressed}}" aria-label="{{b.aria}}" style="height: 44px; padding: 0; background: {{b.bg}}; color: {{b.fg}}; border: 1px solid {{b.border}}; border-radius: 6px; font-family: 'Space Grotesk', sans-serif; font-size: 15px; font-weight: 700; cursor: pointer">{{b.label}}</button>
- </sc-for>
- </div>
- <div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px">
- <sc-for list="{{seqCells}}" as="q" hint-placeholder-count="16">
- <button type="button" onClick="{{q.pick}}" aria-pressed="{{q.pressed}}" aria-label="{{q.aria}}" style="aspect-ratio: 1 / 1; min-height: 72px; padding: 8px; display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; gap: 4px; text-align: left; background: {{q.bg}}; color: #F2F3F5; border: 2px solid {{q.border}}; border-radius: 8px; font: inherit; cursor: pointer; overflow: hidden">
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: {{q.numColor}}">{{q.label}}</span>
- <span style="font-size: 12px; font-weight: 600; line-height: 1.25; overflow: hidden; max-width: 100%; overflow-wrap: anywhere">{{q.name}}</span>
- <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #C9CDD4; min-height: 15px">{{q.meta}}</span>
- </button>
- </sc-for>
- </div>
- <p style="margin: 0; font-size: 12px; color: #A9AEB8">Each pad is one sequence slot, 128 in total. Click an empty slot to add a sequence there.</p>
- </section>
- <section aria-label="Sequence settings" style="flex: 1 1 320px; min-width: 0; background: #1E2126; border: 1px solid #353A43; border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 16px">
- <div style="display: flex; align-items: center; gap: 12px">
- <div style="font-family: 'Space Grotesk', sans-serif; font-size: 28px; font-weight: 700; color: {{accent}}">{{seq.id}}</div>
- <div style="min-width: 0; display: flex; flex-direction: column; gap: 2px">
- <h2 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: #A9AEB8">Sequence settings</h2>
- <div style="font-weight: 600; overflow-wrap: anywhere">{{seq.title}}</div>
- </div>
- </div>
- <sc-if value="{{seq.empty}}" hint-placeholder-val="{{ false }}">
- <div style="display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px dashed #4A505B; border-radius: 8px">
- <p style="margin: 0; color: #C9CDD4">No sequence in this slot.</p>
- <button type="button" onClick="{{addSeq}}" style="height: 44px; padding: 0 16px; align-self: flex-start; background: {{accent}}; color: #15171A; border: 1px solid {{accent}}; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">Add sequence</button>
- </div>
- </sc-if>
- <sc-if value="{{seq.has}}" hint-placeholder-val="{{ true }}">
- <div style="display: flex; flex-direction: column; gap: 16px">
- <div style="display: flex; flex-direction: column; gap: 4px">
- <label for="seq-name" style="font-size: 12px; color: #C9CDD4">Name</label>
- <input id="seq-name" type="text" value="{{seq.name}}" onChange="{{setSeqName}}" style="height: 44px; padding: 0 10px; width: 100%; background: #15171A; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit">
- </div>
- <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px">
- <div style="display: flex; flex-direction: column; gap: 4px">
- <label for="seq-bpm" style="font-size: 12px; color: #C9CDD4">BPM</label>
- <input id="seq-bpm" type="number" min="20" max="300" step="1" value="{{seq.bpm}}" onChange="{{setSeqBpm}}" style="height: 44px; padding: 0 10px; width: 100%; background: #15171A; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font-family: 'IBM Plex Mono', monospace; font-size: 14px">
- </div>
- <div style="display: flex; flex-direction: column; gap: 4px">
- <label for="seq-bars" style="font-size: 12px; color: #C9CDD4">Length (bars)</label>
- <input id="seq-bars" type="number" min="1" step="1" value="{{seq.bars}}" onChange="{{setSeqBars}}" style="height: 44px; padding: 0 10px; width: 100%; background: #15171A; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font-family: 'IBM Plex Mono', monospace; font-size: 14px">
- </div>
- </div>
- <span style="font-size: 12px; color: #A9AEB8">Notes are recorded on the device. This screen sets name, tempo and length only.</span>
- <div style="display: flex; flex-wrap: wrap; gap: 8px; padding-top: 12px; border-top: 1px solid #353A43">
- <button type="button" onClick="{{removeSeq}}" style="height: 44px; padding: 0 16px; background: #2A2D33; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 6px; font: inherit; font-weight: 500; cursor: pointer">Remove sequence</button>
- </div>
- </div>
- </sc-if>
- </section>
- </main>
- </sc-if>
- <footer style="display: flex; flex-wrap: wrap; gap: 8px 24px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">
- <span>{{sampleCount}}</span>
- <span>{{seqCount}}</span>
- <span>WAV files only</span>
- </footer>
- <sc-if value="{{aboutOpen}}" hint-placeholder-val="{{ false }}">
- <div style="position: fixed; top: 0; right: 0; bottom: 0; left: 0; padding: 24px; display: flex; align-items: center; justify-content: center; background: rgba(10, 11, 13, 0.78)">
- <div role="dialog" aria-modal="true" aria-labelledby="about-title" style="width: 100%; max-width: 440px; padding: 24px; display: flex; flex-direction: column; gap: 14px; background: #1E2126; color: #F2F3F5; border: 1px solid #4A505B; border-radius: 12px">
- <h2 id="about-title" style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 22px; font-weight: 700">About mpckit</h2>
- <p style="margin: 0; color: #C9CDD4">A desktop project and kit editor for the MPC Sample. Build kits on your computer, then export them to the device.</p>
- <div style="display: flex; flex-direction: column; gap: 6px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #C9CDD4">
- <span>Version [VERSION]</span>
- <span>[LICENSE]</span>
- <a href="https://git.monoexp.com/bajdev/mpckit">git.monoexp.com/bajdev/mpckit</a>
- </div>
- <p style="margin: 0; font-size: 12px; color: #A9AEB8">An independent project, not affiliated with Akai Professional.</p>
- <button type="button" onClick="{{closeAbout}}" style="height: 44px; padding: 0 20px; align-self: flex-end; background: {{accent}}; color: #15171A; border: 1px solid {{accent}}; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer">Close</button>
- </div>
- </div>
- </sc-if>
- </div>
- </x-dc>
- <script type="text/x-dc" data-dc-script data-props='{"accent":{"editor":"color","default":"#F5A524","options":["#F5A524","#5BC0EB","#9BE564","#FF8A5B"]},"$preview":{"width":1280,"height":860}}'>
- class Component extends DCLogic {
- state = {
- name: 'MyKit',
- aboutOpen: false,
- tab: 'pads',
- bank: 0,
- sel: 0,
- seqBank: 0,
- selSeq: 0,
- pads: {
- 0: { file: 'Kick.wav', frames: 8213, coarse: 0, fine: 0, start: 0, end: 8212, mute: 0 },
- 1: { file: 'Snare.wav', frames: 27000, coarse: 0, fine: 0, start: 0, end: 26999, mute: 0 },
- 2: { file: 'Hat Closed.wav', frames: 22446, coarse: 0, fine: 0, start: 0, end: 22445, mute: 1 },
- 3: { file: 'Hat Open.wav', frames: 108000, coarse: 0, fine: 0, start: 0, end: 107999, mute: 1 },
- 4: { file: 'Kick.wav', frames: 8213, coarse: 12, fine: 40, start: 328, end: 3777, mute: 0 },
- 16: { file: 'Pad Loop Cm.wav', frames: 371368, coarse: 0, fine: 0, start: 0, end: 371367, mute: 0 },
- 17: { file: 'Bass C.wav', frames: 132557, coarse: -2, fine: 0, start: 0, end: 132556, mute: 0 }
- },
- seqs: {
- 0: { name: 'Intro', bpm: 110, bars: 2 },
- 1: { name: 'Verse', bpm: 110, bars: 8 },
- 2: { name: 'Chorus', bpm: 110, bars: 4 }
- }
- };
- renderVals() {
- var self = this;
- var s = this.state;
- var accent = this.props.accent ?? '#F5A524';
- var letters = 'ABCDEFGH';
- var two = function (n) { return n < 10 ? '0' + n : '' + n; };
- var num = function (e) { var v = parseInt(e.target.value, 10); return isNaN(v) ? 0 : v; };
- var signed = function (n) { return (n > 0 ? '+' : '') + n; };
- var setPad = function (patch) {
- self.setState(function (st) {
- var pads = Object.assign({}, st.pads);
- pads[st.sel] = Object.assign({}, pads[st.sel], patch);
- return { pads: pads };
- });
- };
- var setSeq = function (patch) {
- self.setState(function (st) {
- var seqs = Object.assign({}, st.seqs);
- seqs[st.selSeq] = Object.assign({}, seqs[st.selSeq], patch);
- return { seqs: seqs };
- });
- };
- var tuneText = function (p) {
- if (!p.coarse && !p.fine) return '';
- var v = p.coarse + p.fine / 100;
- return (v > 0 ? '+' : '') + v.toFixed(2);
- };
- var countIn = function (map, bank) {
- var c = 0;
- for (var k = 0; k < 16; k++) { if (map[bank * 16 + k]) c++; }
- return c;
- };
- var bankButtons = function (map, current, what, pick) {
- return letters.split('').map(function (label, i) {
- var on = i === current;
- return {
- label: label,
- pressed: on ? 'true' : 'false',
- aria: 'Bank ' + label + ', ' + countIn(map, i) + ' of 16 ' + what,
- bg: on ? accent : '#2A2D33',
- fg: on ? '#15171A' : '#F2F3F5',
- border: on ? accent : '#4A505B',
- pick: function () { pick(i); }
- };
- });
- };
- var tabVals = function (id) {
- var on = s.tab === id;
- return {
- selected: on ? 'true' : 'false',
- fg: on ? '#F2F3F5' : '#A9AEB8',
- line: on ? accent : 'transparent',
- pick: function () { self.setState({ tab: id }); }
- };
- };
- var banks = bankButtons(s.pads, s.bank, 'pads filled', function (i) { self.setState({ bank: i, sel: i * 16 }); });
- var seqBanks = bankButtons(s.seqs, s.seqBank, 'slots used', function (i) { self.setState({ seqBank: i, selSeq: i * 16 }); });
- var cells = [];
- var seqCells = [];
- for (var row = 3; row >= 0; row--) {
- for (var col = 0; col < 4; col++) {
- (function (padNo) {
- var index = s.bank * 16 + padNo - 1;
- var p = s.pads[index];
- var on = index === s.sel;
- var label = letters[s.bank] + two(padNo);
- var badges = '';
- if (p) {
- var parts = [];
- var t = tuneText(p);
- if (t) parts.push(t);
- if (p.mute) parts.push('M' + p.mute);
- badges = parts.join(' · ');
- }
- cells.push({
- label: label,
- name: p ? p.file.replace(/\.wav$/i, '') : '',
- badges: badges,
- pressed: on ? 'true' : 'false',
- aria: 'Pad ' + label + (p ? ', ' + p.file : ', empty'),
- bg: p ? '#333842' : '#22252B',
- border: on ? accent : (p ? '#5A616E' : '#353A43'),
- numColor: on ? accent : '#A9AEB8',
- pick: function () { self.setState({ sel: index }); }
- });
- var qIndex = s.seqBank * 16 + padNo - 1;
- var q = s.seqs[qIndex];
- var qOn = qIndex === s.selSeq;
- var qLabel = letters[s.seqBank] + two(padNo);
- seqCells.push({
- label: qLabel,
- name: q ? q.name : '',
- meta: q ? q.bpm + ' · ' + q.bars + (q.bars === 1 ? ' bar' : ' bars') : '',
- pressed: qOn ? 'true' : 'false',
- aria: 'Sequence slot ' + qLabel + (q ? ', ' + q.name : ', empty'),
- bg: q ? '#333842' : '#22252B',
- border: qOn ? accent : (q ? '#5A616E' : '#353A43'),
- numColor: qOn ? accent : '#A9AEB8',
- pick: function () { self.setState({ selSeq: qIndex }); }
- });
- })(row * 4 + col + 1);
- }
- }
- var sp = s.pads[s.sel];
- var selId = letters[Math.floor(s.sel / 16)] + two(s.sel % 16 + 1);
- var sel = sp ? {
- id: selId, has: true, empty: false, file: sp.file,
- coarse: sp.coarse, fine: sp.fine, mute: '' + sp.mute,
- coarseText: signed(sp.coarse) + ' st', fineText: signed(sp.fine) + ' ct'
- } : {
- id: selId, has: false, empty: true, file: 'Empty',
- coarse: 0, fine: 0, mute: '0', coarseText: '0 st', fineText: '0 ct'
- };
- var sq = s.seqs[s.selSeq];
- var seqId = letters[Math.floor(s.selSeq / 16)] + two(s.selSeq % 16 + 1);
- var seq = sq ? {
- id: seqId, has: true, empty: false, title: 'Sequence ' + two(s.selSeq + 1),
- name: sq.name, bpm: sq.bpm, bars: sq.bars
- } : {
- id: seqId, has: false, empty: true, title: 'Empty slot',
- name: '', bpm: 120, bars: 2
- };
- var files = {};
- Object.keys(s.pads).forEach(function (k) { if (s.pads[k]) files[s.pads[k].file] = true; });
- var seqTotal = Object.keys(s.seqs).filter(function (k) { return s.seqs[k]; }).length;
- return {
- accent: accent,
- name: s.name,
- setName: function (e) { self.setState({ name: e.target.value }); },
- aboutOpen: s.aboutOpen,
- openAbout: function () { self.setState({ aboutOpen: true }); },
- closeAbout: function () { self.setState({ aboutOpen: false }); },
- padsTab: tabVals('pads'),
- seqsTab: tabVals('seqs'),
- showPads: s.tab === 'pads',
- showSeqs: s.tab === 'seqs',
- banks: banks,
- bankSummary: 'Bank ' + letters[s.bank] + ' · ' + countIn(s.pads, s.bank) + ' of 16 filled',
- cells: cells,
- sel: sel,
- setCoarse: function (e) { setPad({ coarse: Math.max(-24, Math.min(24, num(e))) }); },
- setFine: function (e) { setPad({ fine: Math.max(-90, Math.min(90, num(e))) }); },
- setMute: function (e) { setPad({ mute: num(e) }); },
- clearPad: function () {
- self.setState(function (st) {
- var pads = Object.assign({}, st.pads);
- delete pads[st.sel];
- return { pads: pads };
- });
- },
- seqBanks: seqBanks,
- seqBankSummary: 'Bank ' + letters[s.seqBank] + ' · ' + countIn(s.seqs, s.seqBank) + ' of 16 used',
- seqCells: seqCells,
- seq: seq,
- setSeqName: function (e) { setSeq({ name: e.target.value }); },
- setSeqBpm: function (e) { setSeq({ bpm: Math.max(1, num(e)) }); },
- setSeqBars: function (e) { setSeq({ bars: Math.max(1, num(e)) }); },
- addSeq: function () {
- self.setState(function (st) {
- var seqs = Object.assign({}, st.seqs);
- var bpm = 120;
- Object.keys(seqs).forEach(function (k) { if (seqs[k]) bpm = seqs[k].bpm; });
- seqs[st.selSeq] = { name: 'Sequence ' + two(st.selSeq + 1), bpm: bpm, bars: 2 };
- return { seqs: seqs };
- });
- },
- removeSeq: function () {
- self.setState(function (st) {
- var seqs = Object.assign({}, st.seqs);
- delete seqs[st.selSeq];
- return { seqs: seqs };
- });
- },
- sampleCount: Object.keys(files).length + ' of 128 samples',
- seqCount: seqTotal + ' of 128 sequences'
- };
- }
- }
- </script>
- </body>
- </html>
|