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

กลองชุด 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
บน Windows เล่นเสียงผ่าน PowerShell ทุกครั้งที่ตีจะช้าไปเล็กน้อย (เพราะต้องเปิดโปรแกรมใหม่) ถ้าไม่มีเสียงให้ลอง /drum sound engine แผงต้องถูกโฟกัสอยู่ ปุ่มคีย์บอร์ดถึงจะใช้ได้
hooks/register.tsx 244 lines1/**
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