SLOPSHOPPER

drum-th

กลองชุดในแผง ตีด้วยปุ่มคีย์บอร์ด อัดจังหวะ เล่นซ้ำ และ beat สำเร็จรูป ใช้เสียงที่สังเคราะห์เอง

newpanecommandprocesstimeraudio
★ 3v1.0.0MITupdated 2026-10-24tksoft11/claude-code-mods-th/mods/mod-36-drum
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · drum-th
│ ┃ 🥁 Drum ✕ › fix the failing auth test and add an audit log call │ ┃ 🥁 กลองชุด · ตีไปแล้ว 0 ครั้ง │ ┃ ⏺ Read(src/auth.ts) │ ┃ [ 🥁 กระเดื่อง ] [ 🪘 สแนร์ ] [ 🔔 ไฮแฮท ] [ ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ [ ⏺ อัด ] [ ▶ เล่นที่อัด ] [ ⏹ หยุด ] ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /drum │ ⎿ drum-th: 🥁 เปิดแผงกลองแล้ว กด k s h o t l c x บนคีย์บอร์ดเพื่อตี │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🥁 Drum
🥁 กลองชุด · ตีไปแล้ว 0 ครั้ง [ 🥁 กระเดื่อง ] [ 🪘 สแนร์ ] [ 🔔 ไฮแฮท ] [ 🛎️ ไฮแฮทเปิด ] [ [ ⏺ อัด ] [ ▶ เล่นที่อัด ] [ ⏹ หยุด ]
README

Mod 36: Drum TH

กลองชุด 8 ชิ้นในแผงข้าง Claude Code ตีด้วยปุ่มคีย์บอร์ด (k กระเดื่อง, s สแนร์, h ไฮแฮท, o ไฮแฮทเปิด, t ทอมสูง, l ทอมต่ำ, c ปรบมือ, x แฉ) อัดจังหวะแล้วเล่นซ้ำตรงเวลา และมี beat สำเร็จรูป (rock pop funk hiphop ลูกทุ่ง) แบบ step sequencer ปรับ BPM ได้ เสียงในโฟลเดอร์ sounds/ สังเคราะห์ขึ้นเองทั้งหมด

คำสั่ง

คำสั่งทำอะไร
/drumเปิดแผงกลอง
/drum rec / stopอัด / หยุด
/drum play [loop]เล่นสิ่งที่อัดไว้
/drum beat <ชื่อ> [bpm]beat สำเร็จรูป
/drum hit <ชื่อเสียง...>ตีจากคำสั่ง
`/drum sound engine\system`เลือกวิธีเล่นเสียง

ติดตั้ง

/plugin install drum-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. beat แบบ 16 ช่อง
  2. เล่นเสียงข้ามระบบ ($.audio.play หรือ PowerShell/aplay)
  3. step sequencer และเล่นซ้ำด้วย $.clock.after
  4. แผงกลองพร้อมปุ่ม hotkey
  5. คำสั่ง /drum

หมายเหตุ

บน Windows เล่นเสียงผ่าน PowerShell ทุกครั้งที่ตีจะช้าไปเล็กน้อย (เพราะต้องเปิดโปรแกรมใหม่) ถ้าไม่มีเสียงให้ลอง /drum sound engine แผงต้องถูกโฟกัสอยู่ ปุ่มคีย์บอร์ดถึงจะใช้ได้

Source 1 files
hooks/register.tsx 244 lines
1/**
2 * ============================================================================
3 * MOD 36: Drum TH - กลองชุดในแผงข้าง Claude Code (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: drum machine แบบ Roland TR-808 และแอปกลองบนมือถือ
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. แผงกลอง 8 ชิ้น กดด้วยปุ่มคีย์บอร์ด (k = กระเดื่อง, s = สแนร์, h = ไฮแฮท ...)
9 * 2. อัดจังหวะที่ตี แล้วเล่นซ้ำได้
10 * 3. Beat สำเร็จรูป (rock, pop, funk, hiphop, ลูกทุ่ง) แบบ step sequencer 16 ช่อง ปรับ BPM ได้
11 * 4. เสียงกลองในโฟลเดอร์ sounds/ สังเคราะห์ขึ้นเอง (ไม่ใช้ไฟล์ของใคร)
12 *
13 * เรื่องเสียง: $.audio.play เล่นได้บน macOS แต่ terminal ของ Windows/Linux ไม่มีเครื่องเล่น
14 * Mod นี้จึงเล่นผ่าน PowerShell บน Windows (ช้ากว่าเล็กน้อยทุกครั้งที่ตี) และ aplay/paplay บน Linux
15 */
16
17import type { EngineInterface, Register, Timer } from 'claude-code'
18
19interface Pad {
20  id: string
21  key: string // ปุ่มบนคีย์บอร์ด (ตัวพิมพ์เล็ก 1 ตัว)
22  name: string
23  icon: string
24}
25interface Hit {
26  pad: string
27  at: number // มิลลิวินาทีนับจากเริ่มอัด
28}
29
30const PANE = 'drum-th'
31const PADS: Pad[] = [
32  { id: 'kick', key: 'k', name: 'กระเดื่อง', icon: '🥁' },
33  { id: 'snare', key: 's', name: 'สแนร์', icon: '🪘' },
34  { id: 'hihat', key: 'h', name: 'ไฮแฮท', icon: '🔔' },
35  { id: 'openhat', key: 'o', name: 'ไฮแฮทเปิด', icon: '🛎️' },
36  { id: 'tom-high', key: 't', name: 'ทอมสูง', icon: '🟠' },
37  { id: 'tom-low', key: 'l', name: 'ทอมต่ำ', icon: '🟤' },
38  { id: 'clap', key: 'c', name: 'ปรบมือ', icon: '👏' },
39  { id: 'crash', key: 'x', name: 'แฉ', icon: '💥' },
40]
41
42// ==========================================================================
43// บทที่ 1: Beat สำเร็จรูป - 16 ช่องต่อห้อง, "x" = ตี
44// ==========================================================================
45const BEATS: Record<string, { bpm: number; steps: Record<string, string> }> = {
46  rock: { bpm: 110, steps: { kick: 'x.......x.x.....', snare: '....x.......x...', hihat: 'x.x.x.x.x.x.x.x.' } },
47  pop: { bpm: 100, steps: { kick: 'x.....x...x.....', snare: '....x.......x...', hihat: 'xxxxxxxxxxxxxxxx', clap: '............x...' } },
48  funk: { bpm: 96, steps: { kick: 'x..x..x...x..x..', snare: '....x..x.x..x...', hihat: 'x.xxx.xxx.xxx.xx', openhat: '..............x.' } },
49  hiphop: { bpm: 88, steps: { kick: 'x......x..x.....', snare: '....x.......x...', hihat: 'x.x.x.x.x.x.x.xx' } },
50  ลูกทุ่ง: { bpm: 128, steps: { kick: 'x...x...x...x...', snare: '..x...x...x...x.', hihat: 'x.x.x.x.x.x.x.x.', 'tom-low': '..............xx' } },
51}
52
53let isWindows = false
54let isMac = false
55let useEngineAudio = false
56let recording: { startedAt: number; hits: Hit[] } | undefined
57let lastTake: Hit[] = []
58let player: Timer | undefined
59let playingName = ''
60let step = 0
61let hits = 0
62let lastPad = ''
63
64// ==========================================================================
65// บทที่ 2: เล่นเสียง - เลือกวิธีตามระบบ (ส่ง path ผ่าน env กันคำสั่งแทรก)
66// ==========================================================================
67function play($: EngineInterface, padId: string): void {
68  const asset = `sounds/${padId}.wav`
69  if (useEngineAudio || isMac) {
70    void $.audio.play({ asset }).catch(() => undefined)
71    return
72  }
73  const file = `${$.plugin.root}${isWindows ? '\\sounds\\' : '/sounds/'}${padId}.wav`
74  const argv = isWindows
75    ? ['powershell', '-NoProfile', '-NonInteractive', '-Command', '(New-Object Media.SoundPlayer $env:DR_FILE).PlaySync()']
76    : ['sh', '-c', 'paplay "$DR_FILE" 2>/dev/null || aplay -q "$DR_FILE"']
77  // ไม่ await: ให้ตีรัวๆ ได้ เสียงซ้อนกันได้
78  void $.process.run(argv, { env: { DR_FILE: file }, timeoutMs: 10_000 }).catch(() => undefined)
79}
80
81async function hit($: EngineInterface, padId: string): Promise<void> {
82  play($, padId)
83  hits++
84  lastPad = padId
85  if (recording) recording.hits.push({ pad: padId, at: (await $.clock.now()) - recording.startedAt })
86  $.ui.invalidate('ui.render')
87}
88
89// ==========================================================================
90// บทที่ 3: เล่น beat แบบ step sequencer และเล่นซ้ำสิ่งที่อัดไว้
91// ==========================================================================
92function stopPlaying(): void {
93  player?.cancel()
94  player = undefined
95  playingName = ''
96}
97
98function playBeat($: EngineInterface, name: string, bpm: number): void {
99  const beat = BEATS[name]
100  if (!beat) return
101  stopPlaying()
102  playingName = name
103  step = 0
104  const stepMs = Math.round(60_000 / bpm / 4) // 1 จังหวะ = 4 ช่อง (โน้ตเขบ็ตสองชั้น)
105  player = $.clock.every(stepMs, () => {
106    for (const [pad, pattern] of Object.entries(beat.steps)) if (pattern[step % 16] === 'x') play($, pad)
107    step = (step + 1) % 16
108    $.ui.invalidate('ui.render')
109  })
110}
111
112function replay($: EngineInterface, take: Hit[], loop: boolean): void {
113  stopPlaying()
114  if (take.length === 0) return
115  playingName = 'อัดไว้'
116  const length = (take.at(-1)?.at ?? 0) + 500
117  const once = () => {
118    for (const h of take) $.clock.after(h.at, () => play($, h.pad))
119  }
120  once()
121  // วนซ้ำ: เล่นใหม่ทุกครั้งที่ครบความยาวของที่อัด
122  player = loop ? $.clock.every(length, once) : $.clock.after(length, stopPlaying)
123}
124
125// อัด/หยุดอัด (ต้องอยู่ระดับบนสุดของไฟล์ เพราะรับ $ ไป)
126async function toggleRec($: EngineInterface): Promise<string> {
127  if (recording) {
128    lastTake = recording.hits
129    recording = undefined
130    await $.store.set('drum', { lastTake, useEngineAudio })
131    $.ui.invalidate('ui.render')
132    return `⏹️ อัดได้ ${lastTake.length} ครั้ง ยาว ${((lastTake.at(-1)?.at ?? 0) / 1000).toFixed(1)} วิ (/drum play เพื่อฟัง)`
133  }
134  stopPlaying()
135  recording = { startedAt: await $.clock.now(), hits: [] }
136  $.ui.invalidate('ui.render')
137  return '🔴 เริ่มอัด ตีกลองได้เลย แล้ว /drum stop'
138}
139
140const HELP = `🥁 Drum TH (Mod 36)
141========================================
142/drum                     เปิดแผงกลอง (กดปุ่มบนคีย์บอร์ดได้เลย)
143   ${PADS.map(p => `${p.key}=${p.name}`).join('  ')}
144/drum rec | stop          อัด / หยุดอัด (หรือหยุดเล่น)
145/drum play [loop]         เล่นสิ่งที่อัดไว้
146/drum beat <ชื่อ> [bpm]   beat สำเร็จรูป: ${Object.keys(BEATS).join(', ')}
147/drum hit <ชื่อเสียง...>   ตีจากคำสั่ง เช่น /drum hit kick snare
148/drum sound engine|system เลือกวิธีเล่นเสียง (ถ้าไม่มีเสียง ลองสลับดู)`
149
150export const register: Register = on => {
151  on('session.start', async ($, e, next) => {
152    isWindows = /^[A-Za-z]:/.test(e.cwd)
153    isMac = !isWindows && e.cwd.startsWith('/Users/')
154    const saved = (await $.store.get('drum')) as { lastTake?: Hit[]; useEngineAudio?: boolean } | undefined
155    lastTake = saved?.lastTake ?? []
156    useEngineAudio = saved?.useEngineAudio ?? false
157    await $.command.register({ name: 'drum', description: 'กลองชุดในแผง ตี อัด เล่นซ้ำ beat สำเร็จรูป (/drum help)', argumentHint: 'rec|stop|play|beat|hit' })
158    return next(e)
159  })
160
161  on('ui.close', ($, e, next) => {
162    if (e.id === PANE) stopPlaying()
163    return next(e)
164  })
165
166  // ==========================================================================
167  // บทที่ 4: แผงกลอง - ปุ่มแต่ละชิ้นมี hotkey
168  // ==========================================================================
169  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
170    const { Box, Text, Button } = $.ui.resolve(e)
171    const status = recording ? `🔴 กำลังอัด (${recording.hits.length} ครั้ง)` : playingName ? `▶️ ${playingName}${BEATS[playingName] ? ` · ช่อง ${step + 1}/16` : ''}` : `ตีไปแล้ว ${hits} ครั้ง`
172    return (
173      <Box flexDirection="column" gap={1}>
174        <Text bold>🥁 กลองชุด · {status}</Text>
175        <Box flexWrap="wrap" gap={1}>
176          {PADS.map(p => (
177            <Button key={p.id} hotkey={p.key} label={`${p.icon} ${p.name}`} variant={lastPad === p.id ? 'primary' : undefined} onPress={() => void hit($, p.id)} />
178          ))}
179        </Box>
180        <Box gap={1}>
181          <Button key="rec" label={recording ? '⏹ หยุดอัด' : '⏺ อัด'} onPress={() => void toggleRec($)} />
182          <Button key="replay" label="▶ เล่นที่อัด" onPress={() => replay($, lastTake, false)} />
183          <Button key="stop" label="⏹ หยุด" onPress={() => { stopPlaying(); $.ui.invalidate('ui.render') }} />
184        </Box>
185      </Box>
186    )
187  })
188
189  // ==========================================================================
190  // บทที่ 5: คำสั่ง /drum
191  // ==========================================================================
192  on('command.run', { command: 'drum' }, async ($, e) => {
193    const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
194    switch (sub) {
195      case '': {
196        const opened = await $.ui.open({ id: PANE, title: '🥁 Drum', focus: true, closeOnEscape: true })
197        return { text: opened.isPlaced ? `🥁 เปิดแผงกลองแล้ว กด ${PADS.map(p => p.key).join(' ')} บนคีย์บอร์ดเพื่อตี` : `เปิดแผงไม่ได้ (${opened.reason}) ใช้ /drum hit kick snare แทน` }
198      }
199      case 'rec':
200        return { text: recording ? 'กำลังอัดอยู่แล้ว (/drum stop)' : await toggleRec($) }
201      case 'stop':
202        if (recording) return { text: await toggleRec($) }
203        stopPlaying()
204        $.ui.invalidate('ui.render')
205        return { text: '⏹️ หยุดแล้ว' }
206      case 'play':
207        if (lastTake.length === 0) return { text: 'ยังไม่มีสิ่งที่อัดไว้ (/drum rec)' }
208        replay($, lastTake, rest[0] === 'loop')
209        $.ui.invalidate('ui.render')
210        return { text: `▶️ เล่น ${lastTake.length} ครั้ง${rest[0] === 'loop' ? ' วนซ้ำ (/drum stop เพื่อหยุด)' : ''}` }
211      case 'beat': {
212        const [name = '', bpmText] = rest
213        const beat = BEATS[name]
214        if (!beat) return { text: `beat ที่มี: ${Object.keys(BEATS).join(', ')}` }
215        const bpm = Math.min(200, Math.max(50, Number(bpmText) || beat.bpm))
216        playBeat($, name, bpm)
217        await $.ui.open({ id: PANE, title: '🥁 Drum', focus: true, closeOnEscape: true })
218        return { text: `🎶 เล่น ${name} ${bpm} BPM (/drum stop เพื่อหยุด)\n${Object.entries(beat.steps).map(([p, s]) => `  ${p.padEnd(8)} ${s}`).join('\n')}` }
219      }
220      case 'hit': {
221        const pads = rest.filter(r => PADS.some(p => p.id === r || p.key === r)).map(r => PADS.find(p => p.id === r || p.key === r)?.id ?? r)
222        if (pads.length === 0) return { text: `ชื่อเสียง: ${PADS.map(p => p.id).join(', ')}` }
223        for (const p of pads) await hit($, p)
224        return { text: `🥁 ${pads.join(' ')}` }
225      }
226      case 'sound':
227        useEngineAudio = rest[0] === 'engine'
228        await $.store.set('drum', { lastTake, useEngineAudio })
229        return { text: useEngineAudio ? '🔊 เล่นผ่าน Claude Code ($.audio.play)' : '🔊 เล่นผ่านโปรแกรมของระบบ (PowerShell / aplay)' }
230      default:
231        return { text: HELP }
232    }
233  })
234}
235
236/**
237 * ========================================================================
238 * การบ้าน / ต่อยอด
239 * ========================================================================
240 * 1. ทำ step sequencer ในแผง: ตาราง 16 ช่องกดเปิด/ปิดได้ แล้วบันทึกเป็น beat ใหม่
241 * 2. ส่งออกจังหวะที่อัดเป็นไฟล์ MIDI
242 * 3. ตีกลองฉลองเมื่อ test ผ่าน: ดัก tool.call ของ Bash ที่รัน npm test แล้ว exit 0
243 */
244