Main.dc.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>mpckit main screen</title>
  6. <script src="./support.js"></script>
  7. </head>
  8. <body>
  9. <x-dc>
  10. <helmet>
  11. <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&amp;family=Inter:wght@400;500;600&amp;family=Space+Grotesk:wght@500;700&amp;display=swap" rel="stylesheet">
  12. <style>
  13. body{margin:0;background:#15171A}
  14. *{box-sizing:border-box}
  15. a{color:#F5A524}a:hover{color:#FFC35A}
  16. </style>
  17. </helmet>
  18. <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">
  19. <div style="display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid #353A43">
  20. <h1 style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-weight: 700; font-size: 24px; letter-spacing: 0.02em">mpckit</h1>
  21. <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>
  22. </div>
  23. <header style="display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px">
  24. <div style="display: flex; flex-direction: column; gap: 6px; flex: 1 1 240px; min-width: 0; max-width: 360px">
  25. <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>
  26. <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%">
  27. </div>
  28. <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto">
  29. <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>
  30. <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>
  31. <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>
  32. <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>
  33. </div>
  34. </header>
  35. <div role="tablist" aria-label="Views" style="display: flex; gap: 4px; border-bottom: 1px solid #353A43">
  36. <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>
  37. <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>
  38. </div>
  39. <sc-if value="{{showPads}}" hint-placeholder-val="{{ true }}">
  40. <main style="display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start">
  41. <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">
  42. <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
  43. <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>
  44. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">{{bankSummary}}</span>
  45. </div>
  46. <div style="display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px">
  47. <sc-for list="{{banks}}" as="b" hint-placeholder-count="8">
  48. <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>
  49. </sc-for>
  50. </div>
  51. <div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px">
  52. <sc-for list="{{cells}}" as="pad" hint-placeholder-count="16">
  53. <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">
  54. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: {{pad.numColor}}">{{pad.label}}</span>
  55. <span style="font-size: 12px; font-weight: 600; line-height: 1.25; overflow: hidden; max-width: 100%; overflow-wrap: anywhere">{{pad.name}}</span>
  56. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #C9CDD4; min-height: 15px">{{pad.badges}}</span>
  57. </button>
  58. </sc-for>
  59. </div>
  60. <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>
  61. </section>
  62. <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">
  63. <div style="display: flex; align-items: center; gap: 12px">
  64. <div style="font-family: 'Space Grotesk', sans-serif; font-size: 28px; font-weight: 700; color: {{accent}}">{{sel.id}}</div>
  65. <div style="min-width: 0; display: flex; flex-direction: column; gap: 2px">
  66. <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>
  67. <div style="font-weight: 600; overflow-wrap: anywhere">{{sel.file}}</div>
  68. </div>
  69. </div>
  70. <sc-if value="{{sel.empty}}" hint-placeholder-val="{{ false }}">
  71. <div style="display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px dashed #4A505B; border-radius: 8px">
  72. <p style="margin: 0; color: #C9CDD4">This pad is empty. Drop a WAV file on it, or choose one.</p>
  73. <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>
  74. </div>
  75. </sc-if>
  76. <sc-if value="{{sel.has}}" hint-placeholder-val="{{ true }}">
  77. <div style="display: flex; flex-direction: column; gap: 16px">
  78. <div style="display: flex; flex-wrap: wrap; gap: 8px">
  79. <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">
  80. <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>
  81. Play
  82. </button>
  83. <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>
  84. <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>
  85. </div>
  86. <div style="display: flex; flex-direction: column; gap: 8px">
  87. <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>
  88. <div style="display: flex; flex-direction: column; gap: 4px">
  89. <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
  90. <label for="tune-coarse" style="font-size: 12px; color: #C9CDD4">Semitones</label>
  91. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 500; color: #F2F3F5">{{sel.coarseText}}</span>
  92. </div>
  93. <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">
  94. <div aria-hidden="true" style="display: flex; justify-content: space-between; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #A9AEB8">
  95. <span>-24</span>
  96. <span>0</span>
  97. <span>+24</span>
  98. </div>
  99. </div>
  100. <div style="display: flex; flex-direction: column; gap: 4px">
  101. <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
  102. <label for="tune-fine" style="font-size: 12px; color: #C9CDD4">Cents</label>
  103. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 500; color: #F2F3F5">{{sel.fineText}}</span>
  104. </div>
  105. <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">
  106. <div aria-hidden="true" style="display: flex; justify-content: space-between; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #A9AEB8">
  107. <span>-90</span>
  108. <span>0</span>
  109. <span>+90</span>
  110. </div>
  111. </div>
  112. </div>
  113. <div style="display: flex; flex-direction: column; gap: 4px">
  114. <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>
  115. <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">
  116. <option value="0">Off</option>
  117. <option value="1">Group 1</option>
  118. <option value="2">Group 2</option>
  119. <option value="3">Group 3</option>
  120. <option value="4">Group 4</option>
  121. </select>
  122. <span style="font-size: 12px; color: #A9AEB8">Pads in the same group cut each other off on the device.</span>
  123. </div>
  124. <div style="display: flex; flex-wrap: wrap; gap: 8px; padding-top: 12px; border-top: 1px solid #353A43">
  125. <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>
  126. <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>
  127. </div>
  128. </div>
  129. </sc-if>
  130. </section>
  131. </main>
  132. </sc-if>
  133. <sc-if value="{{showSeqs}}" hint-placeholder-val="{{ false }}">
  134. <main style="display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start">
  135. <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">
  136. <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px">
  137. <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>
  138. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">{{seqBankSummary}}</span>
  139. </div>
  140. <div style="display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px">
  141. <sc-for list="{{seqBanks}}" as="b" hint-placeholder-count="8">
  142. <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>
  143. </sc-for>
  144. </div>
  145. <div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px">
  146. <sc-for list="{{seqCells}}" as="q" hint-placeholder-count="16">
  147. <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">
  148. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; color: {{q.numColor}}">{{q.label}}</span>
  149. <span style="font-size: 12px; font-weight: 600; line-height: 1.25; overflow: hidden; max-width: 100%; overflow-wrap: anywhere">{{q.name}}</span>
  150. <span style="font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #C9CDD4; min-height: 15px">{{q.meta}}</span>
  151. </button>
  152. </sc-for>
  153. </div>
  154. <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>
  155. </section>
  156. <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">
  157. <div style="display: flex; align-items: center; gap: 12px">
  158. <div style="font-family: 'Space Grotesk', sans-serif; font-size: 28px; font-weight: 700; color: {{accent}}">{{seq.id}}</div>
  159. <div style="min-width: 0; display: flex; flex-direction: column; gap: 2px">
  160. <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>
  161. <div style="font-weight: 600; overflow-wrap: anywhere">{{seq.title}}</div>
  162. </div>
  163. </div>
  164. <sc-if value="{{seq.empty}}" hint-placeholder-val="{{ false }}">
  165. <div style="display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px dashed #4A505B; border-radius: 8px">
  166. <p style="margin: 0; color: #C9CDD4">No sequence in this slot.</p>
  167. <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>
  168. </div>
  169. </sc-if>
  170. <sc-if value="{{seq.has}}" hint-placeholder-val="{{ true }}">
  171. <div style="display: flex; flex-direction: column; gap: 16px">
  172. <div style="display: flex; flex-direction: column; gap: 4px">
  173. <label for="seq-name" style="font-size: 12px; color: #C9CDD4">Name</label>
  174. <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">
  175. </div>
  176. <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px">
  177. <div style="display: flex; flex-direction: column; gap: 4px">
  178. <label for="seq-bpm" style="font-size: 12px; color: #C9CDD4">BPM</label>
  179. <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">
  180. </div>
  181. <div style="display: flex; flex-direction: column; gap: 4px">
  182. <label for="seq-bars" style="font-size: 12px; color: #C9CDD4">Length (bars)</label>
  183. <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">
  184. </div>
  185. </div>
  186. <span style="font-size: 12px; color: #A9AEB8">Notes are recorded on the device. This screen sets name, tempo and length only.</span>
  187. <div style="display: flex; flex-wrap: wrap; gap: 8px; padding-top: 12px; border-top: 1px solid #353A43">
  188. <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>
  189. </div>
  190. </div>
  191. </sc-if>
  192. </section>
  193. </main>
  194. </sc-if>
  195. <footer style="display: flex; flex-wrap: wrap; gap: 8px 24px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #A9AEB8">
  196. <span>{{sampleCount}}</span>
  197. <span>{{seqCount}}</span>
  198. <span>WAV files only</span>
  199. </footer>
  200. <sc-if value="{{aboutOpen}}" hint-placeholder-val="{{ false }}">
  201. <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)">
  202. <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">
  203. <h2 id="about-title" style="margin: 0; font-family: 'Space Grotesk', sans-serif; font-size: 22px; font-weight: 700">About mpckit</h2>
  204. <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>
  205. <div style="display: flex; flex-direction: column; gap: 6px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #C9CDD4">
  206. <span>Version [VERSION]</span>
  207. <span>[LICENSE]</span>
  208. <a href="https://git.monoexp.com/bajdev/mpckit">git.monoexp.com/bajdev/mpckit</a>
  209. </div>
  210. <p style="margin: 0; font-size: 12px; color: #A9AEB8">An independent project, not affiliated with Akai Professional.</p>
  211. <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>
  212. </div>
  213. </div>
  214. </sc-if>
  215. </div>
  216. </x-dc>
  217. <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}}'>
  218. class Component extends DCLogic {
  219. state = {
  220. name: 'MyKit',
  221. aboutOpen: false,
  222. tab: 'pads',
  223. bank: 0,
  224. sel: 0,
  225. seqBank: 0,
  226. selSeq: 0,
  227. pads: {
  228. 0: { file: 'Kick.wav', frames: 8213, coarse: 0, fine: 0, start: 0, end: 8212, mute: 0 },
  229. 1: { file: 'Snare.wav', frames: 27000, coarse: 0, fine: 0, start: 0, end: 26999, mute: 0 },
  230. 2: { file: 'Hat Closed.wav', frames: 22446, coarse: 0, fine: 0, start: 0, end: 22445, mute: 1 },
  231. 3: { file: 'Hat Open.wav', frames: 108000, coarse: 0, fine: 0, start: 0, end: 107999, mute: 1 },
  232. 4: { file: 'Kick.wav', frames: 8213, coarse: 12, fine: 40, start: 328, end: 3777, mute: 0 },
  233. 16: { file: 'Pad Loop Cm.wav', frames: 371368, coarse: 0, fine: 0, start: 0, end: 371367, mute: 0 },
  234. 17: { file: 'Bass C.wav', frames: 132557, coarse: -2, fine: 0, start: 0, end: 132556, mute: 0 }
  235. },
  236. seqs: {
  237. 0: { name: 'Intro', bpm: 110, bars: 2 },
  238. 1: { name: 'Verse', bpm: 110, bars: 8 },
  239. 2: { name: 'Chorus', bpm: 110, bars: 4 }
  240. }
  241. };
  242. renderVals() {
  243. var self = this;
  244. var s = this.state;
  245. var accent = this.props.accent ?? '#F5A524';
  246. var letters = 'ABCDEFGH';
  247. var two = function (n) { return n < 10 ? '0' + n : '' + n; };
  248. var num = function (e) { var v = parseInt(e.target.value, 10); return isNaN(v) ? 0 : v; };
  249. var signed = function (n) { return (n > 0 ? '+' : '') + n; };
  250. var setPad = function (patch) {
  251. self.setState(function (st) {
  252. var pads = Object.assign({}, st.pads);
  253. pads[st.sel] = Object.assign({}, pads[st.sel], patch);
  254. return { pads: pads };
  255. });
  256. };
  257. var setSeq = function (patch) {
  258. self.setState(function (st) {
  259. var seqs = Object.assign({}, st.seqs);
  260. seqs[st.selSeq] = Object.assign({}, seqs[st.selSeq], patch);
  261. return { seqs: seqs };
  262. });
  263. };
  264. var tuneText = function (p) {
  265. if (!p.coarse && !p.fine) return '';
  266. var v = p.coarse + p.fine / 100;
  267. return (v > 0 ? '+' : '') + v.toFixed(2);
  268. };
  269. var countIn = function (map, bank) {
  270. var c = 0;
  271. for (var k = 0; k < 16; k++) { if (map[bank * 16 + k]) c++; }
  272. return c;
  273. };
  274. var bankButtons = function (map, current, what, pick) {
  275. return letters.split('').map(function (label, i) {
  276. var on = i === current;
  277. return {
  278. label: label,
  279. pressed: on ? 'true' : 'false',
  280. aria: 'Bank ' + label + ', ' + countIn(map, i) + ' of 16 ' + what,
  281. bg: on ? accent : '#2A2D33',
  282. fg: on ? '#15171A' : '#F2F3F5',
  283. border: on ? accent : '#4A505B',
  284. pick: function () { pick(i); }
  285. };
  286. });
  287. };
  288. var tabVals = function (id) {
  289. var on = s.tab === id;
  290. return {
  291. selected: on ? 'true' : 'false',
  292. fg: on ? '#F2F3F5' : '#A9AEB8',
  293. line: on ? accent : 'transparent',
  294. pick: function () { self.setState({ tab: id }); }
  295. };
  296. };
  297. var banks = bankButtons(s.pads, s.bank, 'pads filled', function (i) { self.setState({ bank: i, sel: i * 16 }); });
  298. var seqBanks = bankButtons(s.seqs, s.seqBank, 'slots used', function (i) { self.setState({ seqBank: i, selSeq: i * 16 }); });
  299. var cells = [];
  300. var seqCells = [];
  301. for (var row = 3; row >= 0; row--) {
  302. for (var col = 0; col < 4; col++) {
  303. (function (padNo) {
  304. var index = s.bank * 16 + padNo - 1;
  305. var p = s.pads[index];
  306. var on = index === s.sel;
  307. var label = letters[s.bank] + two(padNo);
  308. var badges = '';
  309. if (p) {
  310. var parts = [];
  311. var t = tuneText(p);
  312. if (t) parts.push(t);
  313. if (p.mute) parts.push('M' + p.mute);
  314. badges = parts.join(' · ');
  315. }
  316. cells.push({
  317. label: label,
  318. name: p ? p.file.replace(/\.wav$/i, '') : '',
  319. badges: badges,
  320. pressed: on ? 'true' : 'false',
  321. aria: 'Pad ' + label + (p ? ', ' + p.file : ', empty'),
  322. bg: p ? '#333842' : '#22252B',
  323. border: on ? accent : (p ? '#5A616E' : '#353A43'),
  324. numColor: on ? accent : '#A9AEB8',
  325. pick: function () { self.setState({ sel: index }); }
  326. });
  327. var qIndex = s.seqBank * 16 + padNo - 1;
  328. var q = s.seqs[qIndex];
  329. var qOn = qIndex === s.selSeq;
  330. var qLabel = letters[s.seqBank] + two(padNo);
  331. seqCells.push({
  332. label: qLabel,
  333. name: q ? q.name : '',
  334. meta: q ? q.bpm + ' · ' + q.bars + (q.bars === 1 ? ' bar' : ' bars') : '',
  335. pressed: qOn ? 'true' : 'false',
  336. aria: 'Sequence slot ' + qLabel + (q ? ', ' + q.name : ', empty'),
  337. bg: q ? '#333842' : '#22252B',
  338. border: qOn ? accent : (q ? '#5A616E' : '#353A43'),
  339. numColor: qOn ? accent : '#A9AEB8',
  340. pick: function () { self.setState({ selSeq: qIndex }); }
  341. });
  342. })(row * 4 + col + 1);
  343. }
  344. }
  345. var sp = s.pads[s.sel];
  346. var selId = letters[Math.floor(s.sel / 16)] + two(s.sel % 16 + 1);
  347. var sel = sp ? {
  348. id: selId, has: true, empty: false, file: sp.file,
  349. coarse: sp.coarse, fine: sp.fine, mute: '' + sp.mute,
  350. coarseText: signed(sp.coarse) + ' st', fineText: signed(sp.fine) + ' ct'
  351. } : {
  352. id: selId, has: false, empty: true, file: 'Empty',
  353. coarse: 0, fine: 0, mute: '0', coarseText: '0 st', fineText: '0 ct'
  354. };
  355. var sq = s.seqs[s.selSeq];
  356. var seqId = letters[Math.floor(s.selSeq / 16)] + two(s.selSeq % 16 + 1);
  357. var seq = sq ? {
  358. id: seqId, has: true, empty: false, title: 'Sequence ' + two(s.selSeq + 1),
  359. name: sq.name, bpm: sq.bpm, bars: sq.bars
  360. } : {
  361. id: seqId, has: false, empty: true, title: 'Empty slot',
  362. name: '', bpm: 120, bars: 2
  363. };
  364. var files = {};
  365. Object.keys(s.pads).forEach(function (k) { if (s.pads[k]) files[s.pads[k].file] = true; });
  366. var seqTotal = Object.keys(s.seqs).filter(function (k) { return s.seqs[k]; }).length;
  367. return {
  368. accent: accent,
  369. name: s.name,
  370. setName: function (e) { self.setState({ name: e.target.value }); },
  371. aboutOpen: s.aboutOpen,
  372. openAbout: function () { self.setState({ aboutOpen: true }); },
  373. closeAbout: function () { self.setState({ aboutOpen: false }); },
  374. padsTab: tabVals('pads'),
  375. seqsTab: tabVals('seqs'),
  376. showPads: s.tab === 'pads',
  377. showSeqs: s.tab === 'seqs',
  378. banks: banks,
  379. bankSummary: 'Bank ' + letters[s.bank] + ' · ' + countIn(s.pads, s.bank) + ' of 16 filled',
  380. cells: cells,
  381. sel: sel,
  382. setCoarse: function (e) { setPad({ coarse: Math.max(-24, Math.min(24, num(e))) }); },
  383. setFine: function (e) { setPad({ fine: Math.max(-90, Math.min(90, num(e))) }); },
  384. setMute: function (e) { setPad({ mute: num(e) }); },
  385. clearPad: function () {
  386. self.setState(function (st) {
  387. var pads = Object.assign({}, st.pads);
  388. delete pads[st.sel];
  389. return { pads: pads };
  390. });
  391. },
  392. seqBanks: seqBanks,
  393. seqBankSummary: 'Bank ' + letters[s.seqBank] + ' · ' + countIn(s.seqs, s.seqBank) + ' of 16 used',
  394. seqCells: seqCells,
  395. seq: seq,
  396. setSeqName: function (e) { setSeq({ name: e.target.value }); },
  397. setSeqBpm: function (e) { setSeq({ bpm: Math.max(1, num(e)) }); },
  398. setSeqBars: function (e) { setSeq({ bars: Math.max(1, num(e)) }); },
  399. addSeq: function () {
  400. self.setState(function (st) {
  401. var seqs = Object.assign({}, st.seqs);
  402. var bpm = 120;
  403. Object.keys(seqs).forEach(function (k) { if (seqs[k]) bpm = seqs[k].bpm; });
  404. seqs[st.selSeq] = { name: 'Sequence ' + two(st.selSeq + 1), bpm: bpm, bars: 2 };
  405. return { seqs: seqs };
  406. });
  407. },
  408. removeSeq: function () {
  409. self.setState(function (st) {
  410. var seqs = Object.assign({}, st.seqs);
  411. delete seqs[st.selSeq];
  412. return { seqs: seqs };
  413. });
  414. },
  415. sampleCount: Object.keys(files).length + ' of 128 samples',
  416. seqCount: seqTotal + ' of 128 sequences'
  417. };
  418. }
  419. }
  420. </script>
  421. </body>
  422. </html>