เวลาเราเขียน ui.tone(60, ui.WAVE_SQUARE, ...) หนึ่งบรรทัด เบื้องหลังบอร์ดทำงานเป็นทอด ๆ แบบนี้:

เราคุมแค่ 2 อย่าง คือ โน้ตไหน (note) กับ รูปคลื่นไหน (wave) ที่เหลือ — แปลงเป็นความถี่, หยิบรูปคลื่น, ส่งเข้า DAC — บอร์ดจัดการให้หมด นี่คือเหตุผลที่เล่นเสียงได้โดยไม่ต้องรู้ฮาร์ดแวร์ลึก
Genesis มี CPU ตัวที่สอง (Z80) ไว้ทำเสียงโดยเฉพาะ ปลด CPU หลัก (68000) ให้ทุ่มกับ gameplay — แบ่งงานให้ชิปที่ถนัด
เครื่องยุค 8-bit หลายรุ่น (Master System, ColecoVision, BBC Micro) ใช้ชิปเสียงตัวเดียวกันคือ SN76489 ในรูป มันสร้างเสียง square wave ได้โดยตรง — นี่แหละต้นกำเนิดน้ำเสียง "เป็นเกม" ที่เราจะลองทำวันนี้

ที่มา: "SN76489" — Evan-Amos, Public domain, Wikimedia Commons
เชื่อมกับวันนี้: บอร์ดเราก็มี audio codec แยกทำเสียงจริง (ไม่ใช่ buzzer) ปล่อยให้ core เกมไม่สะดุด — partitioning แนวคิดเดียวกันกับ Genesis เมื่อ 30 กว่าปีก่อน
duty cycle ตัวเดียวกับที่ทำเสียง เอามาคุม ความสว่าง LED ได้เลย: gpio.led(n).brightness(0–100)
import gpio, time
led = gpio.led(0)
# ไฟหายใจ (breathing) — สว่างขึ้น/หรี่ลง นุ่ม ๆ
while True:
for b in range(0, 101, 2):
led.brightness(b); time.sleep_ms(15)
for b in range(100, -1, -2):
led.brightness(b); time.sleep_ms(15)
brightness(0)ดับ ·100สว่างเต็ม · ระหว่างนั้นหรี่ — เบื้องหลังคือ PWM ~10 kHz ที่ตามองเป็นความสว่างต่อเนื่อง (ต้องใช้เฟิร์มแวร์ Game รุ่นล่าสุดที่เปิด LED PWM)

โน้ตเดียวกัน (ความถี่เท่ากัน) แต่ รูปคลื่นต่างกัน → น้ำเสียงต่างกัน
นี่คือเหตุผลที่กีตาร์กับเปียโนเล่นโน้ตเดียวกันแล้วเสียงไม่เหมือนกัน
โน้ตเดิม เปลี่ยนแค่รูปคลื่น — ดูทรงคลื่น "morph" ไปมา (square sine) ความถี่คงเดิม:

มี TRIANGLE / SAW ด้วย น้ำเสียงก้ำกึ่งระหว่างสองแบบนี้ คาบนี้เราโฟกัส SQUARE กับ SINE ก่อน
โน้ตเดียวกัน (ความถี่เท่ากัน) เปลี่ยนแค่ wave รูปคลื่นที่ลำโพงวาดก็เปลี่ยนรูปทรง — นี่คือสิ่งที่หูเราตีความเป็น "น้ำเสียง" ที่ต่างกัน:

| รูปคลื่น | ทรง | ความรู้สึก |
|---|---|---|
WAVE_SQUARE |
สี่เหลี่ยม | กระด้าง เป็นเกม 8-bit |
WAVE_SINE |
โค้งนุ่ม | กลม นุ่มนวล |
WAVE_TRIANGLE |
สามเหลี่ยม | นุ่มแต่มีเหลี่ยม |
WAVE_SAW |
ฟันเลื่อย | คม สดใส แหลม |
ทั้ง 4 ตัวนี้เรียกผ่าน
ui.WAVE_*ได้จริงบนบอร์ด ลองสลับดูว่าเสียงของกลุ่มเราเข้ากับแบบไหน
ui.tone ไม่ต้องจำความถี่เป็นเฮิรตซ์ ใช้ เลข MIDI ที่จำง่ายกว่า:
| เลข MIDI | โน้ต | หมายเหตุ |
|---|---|---|
| 60 | C4 (โดกลาง) | จุดอ้างอิงที่จำง่าย |
| 62 | D4 (เร) | +2 = เต็มเสียง |
| 64 | E4 (มี) | |
| 65 | F4 (ฟา) | +1 = ครึ่งเสียง |
| 67 | G4 (ซอล) |
กติกาที่ต้องจำ:
อยากได้เสียงเดิมแต่สูงขึ้นทั้งชุด? บวก 12 เข้าไปทุกโน้ตได้เลย

เลข MIDI เป็นแค่ดัชนี บอร์ดต้องแปลงเป็น ความถี่จริง (Hz) ก่อนถึงเล่นได้ สูตรที่เฟิร์มแวร์ใช้จริงคือ:
โน้ต 69 คือ A4 = เสียงอ้างอิงมาตรฐาน 440 Hz ทุกครั้งที่บวกโน้ตขึ้น 12 ตัว เลขชี้กำลังเพิ่มขึ้น 1 พอดี:
และ +1 โน้ตคือคูณด้วย — นี่คือที่มาของคำว่า "ครึ่งเสียง"

เส้นโค้งไม่ใช่เส้นตรง เพราะหูเรารับ "ระดับเสียง" เป็นอัตราส่วน (เท่าตัว) ไม่ใช่ผลบวก — คณิตของเสียงจึงเป็น เลขชี้กำลัง ไม่ใช่เส้นตรง
ui.tone(note, wave, velocity, ms)
| พารามิเตอร์ | ความหมาย | ช่วงค่า |
|---|---|---|
note |
เลข MIDI ของโน้ต | 60 = โดกลาง |
wave |
รูปคลื่น | ui.WAVE_SQUARE / ui.WAVE_SINE |
velocity |
ความดัง | 0–127 |
ms |
ความยาวเสียง (มิลลิวินาที) | เช่น 200 |
เคล็ดลับ:
ui.toneเล่นทีละโน้ต เราต้อง หน่วงเวลาเอง ระหว่างโน้ต ไม่งั้นมันจะเกยกันจนฟังไม่เป็นจังหวะ

ทฤษฎี → โค้ด (bridge):
velocity = 100ที่เห็นในui.tone(note, ui.WAVE_SQUARE, 100, NOTE_MS)(s08_sound.py:37,45) คือค่าความดังในสเกล MIDI 0–127 (ตารางข้างบน) —100≈ ดังราว 79% ลองลดเหลือ40แล้วฟังว่าเบาลง- สูตรแปลงเลข MIDI → ความถี่
f(n)=440·2^((n−69)/12)และการหยิบ wavetable มาวาดเป็นสัญญาณ ไม่มีบรรทัด Python บรรทัดไหนทำ — มันรันใน C engine ของบอร์ด บรรทัด Python ที่ใกล้ที่สุดคือui.tone(...)(s08_sound.py:37,45) ที่ "สั่ง" ให้ engine ไปคำนวณให้ นี่คือ abstraction ที่เรายอมรับ: เราคุมแค่note+wave+velocity+msที่เหลือ engine จัดการหมด
ui.sfx(id) คือเสียงที่ C engine บนบอร์ดเตรียมไว้ให้แล้ว เราแค่หยิบ id มาเรียก
ไม่ต้องจำความถี่ ไม่ต้องแต่งเอง
import ui
import time
# id พวกนี้นิยามไว้ในโมดูล ui แล้ว เราแค่หยิบมาใช้
print("เล่น sfx: เริ่มเกม")
ui.sfx(ui.SFX_UI_START)
time.sleep_ms(600)
print("เล่น sfx: กินอาหาร (snake)")
ui.sfx(ui.SFX_SNAKE_EAT)
time.sleep_ms(400)
print("เล่น sfx: เกมจบ")
ui.sfx(ui.SFX_GAME_OVER)
time.sleep_ms(900)

รันโค้ดข้างบนใน BENTO IDE → คอนโซลพิมพ์ เล่น sfx: ... ทีละบรรทัด พร้อมกับเสียงดังจากลำโพงบนบอร์ด
ใส่
sleep_msคั่น เพื่อให้แต่ละเสียงเล่นจบก่อนเริ่มเสียงถัดไป — ไม่งั้นมันจะตัดทับกัน
เชื่อมกับเกมของเรา: ในเกมที่เขียนด้วยโมดูล
bentogameเราเรียกเสียงด้วย ชื่อ เช่นgame.sfx("eat")/game.sfx("die")— เบื้องหลังมันเรียกui.sfx(...)ที่เรียนในคาบนี้ให้เอง (เสียงเดียวกัน แค่ห่อด้วยชื่อที่จำง่ายกว่าเลข id) ความเข้าใจคาบนี้จึงใช้ได้กับทุกเกม
ตัวอย่าง id เสียงสำเร็จรูป (หยิบมาจากโมดูล ui ตรง ๆ):
| id | ใช้ตอนไหน |
|---|---|
ui.SFX_UI_START |
เริ่มเกม / กดเข้าเมนู |
ui.SFX_SNAKE_EAT |
กินอาหาร / เก็บไอเทม / ทำแต้ม |
ui.SFX_GAME_OVER |
เกมจบ / แพ้ |
เราใช้ชื่อ id ที่บอร์ดตั้งไว้ ไม่ต้องจำตัวเลข — โค้ดอ่านง่ายและสื่อความหมายชัด
ใน Pong เราจะยืมเสียงเหล่านี้มาใช้ (เช่นSFX_SNAKE_EATตอนบอลกระทบไม้)
ทีนี้มาแต่งทำนองเอง ทำนองนี้คือ "โด เร มี ฟา ซอล" (C major ครึ่งแรก)
# ทำนองนี้คือ "โด เร มี ฟา ซอล" (C major scale ครึ่งแรก)
melody = [60, 62, 64, 65, 67] # เลข MIDI ของแต่ละโน้ต
NOTE_MS = 200 # โน้ตละ 200 ms
print("เล่นทำนองด้วย WAVE_SQUARE (เสียงเกมคลาสสิก)")
for note in melody:
ui.tone(note, ui.WAVE_SQUARE, 100, NOTE_MS)
time.sleep_ms(NOTE_MS + 20) # +20 ms กันโน้ตเกยกัน ฟังเป็นจังหวะ
เก็บทำนองเป็น
listของเลข MIDI แล้ววนforเล่นทีละโน้ต — เพิ่ม/ลด/สลับโน้ตได้ง่ายมาก
แต่ละค่าใน melody คือ "ระดับเสียง" — เรียงในลิสต์อย่างไร เสียงก็ไต่ขึ้นตามนั้น:
โน้ต
+1(65→F4) คือก้าว ครึ่งเสียง จึงไต่ขึ้นน้อยกว่า+2ที่เป็นเต็มเสียง — ลำดับในลิสต์ = เส้นทางขึ้นของทำนองพอดี
เสียงทั้ง 21 ตัว สังเคราะห์ตรงจากตารางจริงใน firmware (bento_sfx.c) — กดเลือกเสียง → กดเล่น → ลากแถบ ซูม ดูรูปคลื่นราย cycle (เห็นทรง square / triangle / saw ชัด) → เทียบ spectrogram (ความถี่ตามเวลา = ทำนอง/การกวาด + harmonics)
ลากแถบ ซูม: ขวา = น้อย cycle (เห็นทรงคลื่นชัด เช่น square ของ snake_eat) · ซ้าย = หลาย cycle (เห็น envelope / การไล่โน้ต) · spectrogram โชว์การกวาดความถี่จริง (
ยิงเลเซอร์saw กวาดลง,Pong ชนะ4 โน้ตขึ้น) · เครื่องมือเต็มแยกหน้า: sound_video_demo
โน้ตเดิมทั้งหมด เปลี่ยนแค่ wave จาก SQUARE เป็น SINE แล้วฟังว่าน้ำเสียงต่างกันแค่ไหน
time.sleep_ms(300)
# ลองเปลี่ยน "รูปคลื่น" แล้วฟังว่าน้ำเสียงต่างกันอย่างไร (โน้ตเดิมทั้งหมด)
print("เล่นทำนองเดิมด้วย WAVE_SINE (เสียงนุ่มขึ้น)")
for note in melody:
ui.tone(note, ui.WAVE_SINE, 100, NOTE_MS)
time.sleep_ms(NOTE_MS + 20)
print("จบแล็บเสียง — ลองแก้ melody / wave / NOTE_MS ดูว่าผลลัพธ์เปลี่ยนยังไง")
ui.WAVE_SQUARE

ui.WAVE_SINE
นี่คือหัวใจของแล็บ: เปลี่ยนตัวแปรเดียว (wave) ผลลัพธ์เปลี่ยนทั้งความรู้สึก — นี่แหละ waveform (รูปคลื่นข้างบน morph ตามที่เราสลับ
wave)
เสียงที่ดี "เล่าเรื่อง" ได้ — โน้ตพุ่งขึ้น = "ได้ของ!" · โน้ตตก = "พลาด / เจ็บ"
ลองทำ helper เล็ก ๆ ที่เล่นทำนองจาก list ของโน้ต แล้วออกแบบเสียงของกลุ่มเอง:
import ui, time
# helper: เล่นโน้ตทีละตัวจาก list (หน่วงเวลาให้เอง)
def play(notes, wave=ui.WAVE_SQUARE, ms=110, gap=20):
for n in notes:
ui.tone(n, wave, 110, ms)
time.sleep_ms(ms + gap) # ถ้าไม่หน่วง โน้ตจะเกยกัน
coin = [72, 76, 79] # โน้ตพุ่งขึ้น = เสียง "ได้เหรียญ"
hurt = [60, 55] # โน้ตตก = เสียง "เจ็บ"
play(coin) # ฟังเสียงที่กลุ่มเราออกแบบเอง
ลองสลับเลขใน
coin/hurtฟังว่าชอบแบบไหน — นี่คือเสียง "ของกลุ่มเรา" ไม่ใช่เสียงสำเร็จรูป

มีเสียงสำเร็จรูป ครบ 21 เสียง เรียกได้ด้วยชื่อง่าย ๆ game.sfx("ชื่อ") — เป็นชุดเดียวกับที่เกม C บนบอร์ดใช้จริง:
| เกม | ชื่อเสียง |
|---|---|
| Snake | eat · turn · die |
| Flappy | flap · point · fall |
| Pong | wall · paddle · pong_score · win · lose |
| Shooter | fire · hit · explode · lose_life |
| UI / ทั่วไป | start · select · back · deny · move · gameover |
import bentogame as game
game.sfx("explode") # เสียงระเบิดเดียวกับเกม Shooter ตัวจริง
game.sfx("fall") # นกตก (Flappy) — เรียกด้วยชื่อได้ทุกตัวแล้ว
ครบทั้ง 21 เสียง เรียกด้วยชื่อได้หมด — ลองหยิบไปใส่เกมของกลุ่มให้เข้ากับจังหวะ (เก็บแต้ม / ชน / แพ้)
โค้ดเต็มอยู่ที่ practise_codes/s08_sound.py↗ (เว้นช่องให้เราเติม)
ลองเติมเองให้สุดก่อน เดี๋ยวเฉลยพร้อมกันในห้องเรียน
ขั้นตอนรัน:

ปุ่ม Program to Device ใน BENTO IDE — flash โค้ดลงบอร์ดเพื่อให้เสียงดังจริง
เราใช้ Program to Device เสมอ ไม่ใช้
exec(open(...))— ให้บอร์ดรันไฟล์โดยตรง
แนวคิดเดียวกันนี้เอาไปต่อใน Pong ได้ทันที วางเสียงตาม "เหตุการณ์" ในเกม:
| เหตุการณ์ใน Pong | เสียงที่ใส่ |
|---|---|
| เริ่มเกม / เริ่มรอบใหม่ | ui.sfx(ui.SFX_UI_START) |
| บอลกระทบไม้ตี | ui.sfx(ui.SFX_SNAKE_EAT) (เสียง "ป๊อก") |
| ทำแต้ม / ฝ่ายตรงข้ามพลาด | ทำนองสั้นด้วย ui.tone |
| จบเกม | ui.sfx(ui.SFX_GAME_OVER) |

Pong จริงบนบอร์ด — แต่ละจุดสีคือ "เหตุการณ์ → เสียง" ที่เราวาง
# ตัวอย่างวางในลูปเกม Pong: บอลชนไม้ตี
if ball_hits_paddle:
ui.sfx(ui.SFX_SNAKE_EAT) # เสียงตอบรับว่าตีโดน
วางเสียงตรง "จุดที่เกิดเหตุการณ์" ในลูป — เสียงคือ feedback ที่ทำให้เกมรู้สึกมีชีวิต
| อาการ | สาเหตุ | แก้ |
|---|---|---|
| ไม่ได้ยินเสียงเลย | รันบน simulator | ต้อง Program to Device ลงบอร์ดจริง |
| เสียงตัดทับกัน | ลืม sleep_ms คั่น |
หน่วง NOTE_MS + 20 ระหว่างโน้ต |
AttributeError: WAVE_SQUARE |
สะกดผิด / ลืม ui. |
ใช้ ui.WAVE_SQUARE |
| ทำนองเร็ว/ช้าผิด | ปรับ NOTE_MS ผิดที่ |
แก้ค่าเดียวที่ NOTE_MS |
| โน้ตเพี้ยน | ใส่ค่า note ผิด | 60 = โดกลาง, +1 = ครึ่งเสียง |
ถ้าโปรแกรมค้างให้กด
Ctrl-Cแล้ว Program to Device ใหม่อีกครั้ง
โปรแกรมเดินเป็นเส้นตรง: เล่น sfx 3 เสียง → วนทำนอง SQUARE → วนทำนอง SINE → จบ ทุกครั้งที่เรียก ui.tone / ui.sfx หนึ่งบรรทัด งานหนัก (แปลงเลข → รูปคลื่น → DAC) จะ ข้ามเส้นไปทำใน C engine ของบอร์ด แล้วออกลำโพง:
เส้นบน = control flow ของโปรแกรม Python (บรรทัดไหนทำก่อน-หลัง) · เส้นล่าง = signal chain ที่ engine ทำให้เบื้องหลังทุกครั้งที่เราสั่งหนึ่งโน้ต — นี่คือเหตุผลที่เราเขียนเสียงได้โดยไม่ต้องแตะ DAC เอง
แล็บเล็ก ๆ วันนี้แตะ "รากฐาน" ของวิศวกรรมฝังตัวครบทั้ง 4 ด้าน — เสียง 1 โน้ตคือจุดที่ทุกอย่างมาเจอกัน:
| ด้าน | วันนี้เราใช้มันตรงไหน |
|---|---|
| Embedded / MCU | PWM / Timer สร้างความถี่ · audio DAC แปลงดิจิทัลเป็นแรงดัน · การ partition งานเสียงออกจาก core เกม |
| Python | เก็บทำนองเป็น list ของเลข MIDI · วน for เล่นทีละโน้ต · เขียน function play() รับพารามิเตอร์ |
| Algorithms / DSP | แปลงเลข MIDI → ความถี่ด้วยสูตร เลขชี้กำลัง · wavetable synthesis หยิบรูปคลื่นมาวาดเป็นสัญญาณ |
| Graphics / สัญญาณ | มองรูปคลื่นเป็น "กราฟตามเวลา" — แกนเดียวกับที่เราใช้วาดภาพ แค่เปลี่ยนจากพิกเซลเป็นแรงดัน |
Can you hear the difference between a sine wave and a square wave? — Audio Masterclass
How Oldschool Sound/Music worked — The 8-Bit Guy
เสียงทำให้เกมรู้สึกมีชีวิต แต่เบื้องหลังคือ PWM, DAC, list, for, และคณิตของความถี่ — ทักษะเดียวกันนี้ไปต่อยอดเป็นสินค้าจริงได้ (เครื่องดนตรีดิจิทัล, อุปกรณ์แจ้งเตือน, ของเล่นอัจฉริยะ)
วันนี้เราได้
ui.sfx(id) — เสียงสำเร็จรูปui.tone(note, wave, velocity, ms) — แต่งทำนองเองทำเอง 30% (เติมใน practise)
coin + hurt ของกลุ่มเอง ด้วย play() แล้วเอาไปใส่ Pong+12 ทุกโน้ต)คาบหน้าเราจะเอาทักษะทั้งหมด (วาด · จอย · เสียง) มาประกอบเป็นเกมเต็มในโฟลเดอร์
full_games/
s08_sound.py — อ่านให้เข้าใจแล้วพิมพ์เองเฉลยนี้มีไว้ให้ "เทียบ" ไม่ใช่ให้ลอกวางส่ง คะแนนของคาบนี้อยู่ที่ใบงานและการอธิบายด้วยคำพูดของน้องเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะทีละส่วน เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนแรก — เล่นเสียงสำเร็จรูปด้วย id เดียว:
# --- ส่วนที่ 1: เสียงสำเร็จรูป (sfx) เรียกด้วย id ที่บอร์ดเตรียมไว้ให้ ---
# id พวกนี้นิยามไว้ในโมดูล ui แล้ว เราแค่หยิบมาใช้ (ไม่ต้องจำความถี่เอง)
print("เล่น sfx: เริ่มเกม")
ui.sfx(ui.SFX_UI_START)
time.sleep_ms(600)
print("เล่น sfx: กินอาหาร (snake)")
ui.sfx(ui.SFX_SNAKE_EAT)
time.sleep_ms(400)
print("เล่น sfx: เกมจบ")
ui.sfx(ui.SFX_GAME_OVER)
time.sleep_ms(900)
ui.sfx(ui.SFX_UI_START) แทนที่จะแต่งเสียงเริ่มเกมเอง? เพราะเสียงพวกนี้ C engine บนบอร์ดสังเคราะห์ไว้ให้แล้ว ตรงกับสไลด์ "ส่วนที่ 1 — เสียงสำเร็จรูป ui.sfx" — เราหยิบ id มาใช้ ไม่ต้องจำความถี่ ไม่ต้องแตะ DAC เองsleep_ms สามค่าไม่เท่ากัน: 600 (:19), 400 (:23), 900 (:27) — แต่ละเสียงยาวไม่เท่ากัน เราหน่วงให้เสียงก่อนหน้า "เล่นจบ" ก่อนค่อยสั่งเสียงถัดไป นี่คือกับดัก "เสียงตัดทับกัน" ที่เราเตือนไว้ในสไลด์กับดัก ถ้าลืมหน่วง sfx จะเกยกันจนฟังไม่รู้เรื่องSFX_UI_START / SFX_SNAKE_EAT / SFX_GAME_OVER) แทนตัวเลข ทำให้โค้ด อ่านแล้วรู้เลยว่าเสียงอะไร — นิสัยตั้งชื่อให้สื่อความหมาย ไม่ฝังเลขดิบ# --- ส่วนที่ 2: แต่งทำนองเองด้วย tone ---
# ui.tone(note, wave, velocity, ms) — เล่นทีละโน้ต เราหน่วงเวลาเองให้โน้ตไม่ทับกัน
# ทำนองนี้คือ "โด เร มี ฟา ซอล" (C major scale ครึ่งแรก)
melody = [60, 62, 64, 65, 67] # เลข MIDI ของแต่ละโน้ต
NOTE_MS = 200 # โน้ตละ 200 ms
print("เล่นทำนองด้วย WAVE_SQUARE (เสียงเกมคลาสสิก)")
for note in melody:
ui.tone(note, ui.WAVE_SQUARE, 100, NOTE_MS)
time.sleep_ms(NOTE_MS + 20) # +20 ms กันโน้ตเกยกัน ฟังเป็นจังหวะ
list (:32) แล้ววน for (:36) แทนที่จะเขียน ui.tone ห้าบรรทัดเรียงกัน? เพราะพอทำนองอยู่ใน ข้อมูล เราเพิ่ม/ลด/สลับโน้ตได้โดยไม่แตะลูปเลย — แนวคิดเดียวกับ "ขับ pattern ด้วยข้อมูล" ตรงกับตาราง melody = [60,62,64,65,67] ในสไลด์ "แต่งทำนองด้วย ui.tone"60, 62, 64, 65, 67 คือ เลข MIDI จากสไลด์ "โน้ตเป็นตัวเลข" — จำได้ไหมว่า +2 = เต็มเสียง +1 = ครึ่งเสียง? นั่นคือเหตุผลที่ช่วง 64→65 (มี→ฟา) ไต่ขึ้นน้อยกว่าช่วงอื่นNOTE_MS = 200 ไว้ ที่เดียว (:33) แล้วอ้างซ้ำทั้งความยาวโน้ตและช่วงหน่วง — อยากให้ทำนองเร็ว/ช้า แก้เลขเดียวจบ ไม่ต้องไล่แก้ทุกบรรทัด นี่คือการแยก "ค่า" ออกจาก "โครง"time.sleep_ms(NOTE_MS + 20) (:38) คือหัวใจของจังหวะ: ui.tone เล่นทีละโน้ตแล้วคืนคุมกลับมาทันที ถ้าไม่หน่วง โน้ตถัดไปจะทับโน้ตเดิม +20 คือช่องว่างเล็ก ๆ ให้ได้ยินเป็น "โน้ตแยกกัน"
velocity = 100ในสเกล MIDI 0–127 คือความดังราว 79% ที่เราคุยกันในสไลด์พารามิเตอร์ — ลองลดเหลือ40แล้วฟังว่าเบาลงจริงไหม เปลี่ยนทีละค่าแล้วเชื่อหูตัวเอง
time.sleep_ms(300)
# ลองเปลี่ยน "รูปคลื่น" แล้วฟังว่าน้ำเสียงต่างกันอย่างไร (โน้ตเดิมทั้งหมด)
print("เล่นทำนองเดิมด้วย WAVE_SINE (เสียงนุ่มขึ้น)")
for note in melody:
ui.tone(note, ui.WAVE_SINE, 100, NOTE_MS)
time.sleep_ms(NOTE_MS + 20)
print("จบแล็บเสียง — ลองแก้ melody / wave / NOTE_MS ดูว่าผลลัพธ์เปลี่ยนยังไง")
ui.WAVE_SQUARE → ui.WAVE_SINE (:45) โน้ตเดิม จังหวะเดิม แต่คนละน้ำเสียง — นี่คือบทเรียนหลักของแล็บที่เราพูดในสไลด์ "ลองเปลี่ยนรูปคลื่น": เปลี่ยนตัวแปรเดียว ผลลัพธ์เปลี่ยนทั้งความรู้สึกmelody ตัวเดิมซ้ำ (:44) ไม่ต้องประกาศทำนองใหม่ — นี่คือผลพลอยได้ของการเก็บทำนองเป็น list ตั้งแต่แรก อยากเทียบ SQUARE กับ SINE บนทำนองเดียวกัน ก็แค่วนลิสต์เดิมอีกรอบtime.sleep_ms(300) คั่นระหว่างสองรอบ (:40) เว้นช่องให้หูตั้งหลักก่อนฟังน้ำเสียงใหม่ ไม่ให้ SQUARE กับ SINE ต่อกันจนแยกไม่ออกprint ปิดท้าย (:48) ชวนให้ไปแก้ melody / wave / NOTE_MS ต่อ — เฉลยไม่ได้ให้จบแค่รันผ่าน แต่ชี้ว่า "สามลูกบิด" ไหนที่น้องหมุนเล่นได้ทำไมเราถึงยืนยันให้เปลี่ยน "ทีละตัวแปร"? เพราะถ้าเปลี่ยนโน้ต จังหวะ และรูปคลื่นพร้อมกัน พอเสียงเพี้ยนจะไม่รู้ว่าตัวไหนทำให้เพี้ยน การแยกตัวแปรคือวิธีคิดแบบวิศวกร ไม่ใช่แค่เรื่องเสียง
สี่ชั้นของแล็บนี้ถูกจัดให้ เพิ่มการควบคุมทีละขั้น จาก "เสียงสำเร็จรูปที่บอร์ดทำให้" ไปจนถึง "เสียงที่เราคุมเองทุกพารามิเตอร์" แล้ววนกลับมาที่ชื่อเรียกที่อ่านง่าย:
| ชั้น | โค้ด (บรรทัด) | สิ่งใหม่ที่คุมได้ | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| 1 sfx สำเร็จรูป | ui.sfx(id) :18,22,26 |
เรียกเสียงทั้งก้อนด้วย id เดียว | sleep_ms คั่นกันเสียงทับ |
| 2 แต่งทำนอง | ui.tone + melody + for :32,36-38 |
คุม note เอง · list ขับ pattern |
for + sleep (คาบ 1) |
| 3 เปลี่ยนน้ำเสียง | wave=ui.WAVE_SINE :45 |
เปลี่ยนตัวแปรเดียว น้ำเสียงเปลี่ยนหมด | melody เดิม · ลูปเดิม |
| 4 เรียกด้วยชื่อ | game.sfx("eat") :51 |
ห่อ id ด้วยชื่ออ่านง่าย ครบ 21 เสียง | ui.sfx เดิม (แค่ห่อชื่อ) |
# --- เมนูเสียงสำเร็จรูปทั้งหมด 21 เสียง (หยิบไปใส่เกมได้เลย) ---
# วิธีง่ายสุด: import bentogame as game แล้ว game.sfx("ชื่อ")
# Snake : "eat" "turn" "die"
# Flappy: "flap" "point" "fall"
# Pong : "wall" "paddle" "pong_score" "win" "lose"
# Shoot : "fire" "hit" "explode" "lose_life"
# UI : "start" "select" "back" "deny" "move" "gameover"
game.sfx("eat") :51) วนกลับมาหา id เดิม แค่ห่อด้วยชื่อที่จำง่ายกว่าเลข — เหมาะเวลาไปใส่เกมจริงถ้ามองสี่ชั้นนี้ออกว่า "มันคือระดับการควบคุมที่ค่อย ๆ เพิ่ม" น้องจะเลือกได้เองว่างานไหนหยิบ
ui.sfxพอ งานไหนต้องui.toneคุมเอง — นั่นคือดุลยพินิจของวิศวกร ไม่ใช่ท่องจำ API
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่อง "มองให้ทะลุ" ว่าเสียงหนึ่งโน้ตที่เราเพิ่งทำ จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — เสียงนี้ยืนอยู่บนอะไร ย้อนกลับไปสไลด์ "บอร์ดสร้างเสียงได้อย่างไร — PWM" กับ "PWM ไม่ได้มีแค่เสียง — หรี่ไฟ LED ด้วย" เราเห็นว่าเสียงเกิดจากการ เปิด-ปิดขาเร็ว ๆ แล้วเลือกรูปคลื่น และ duty cycle ตัวเดียวกันนั้นเอาไปหรี่ไฟ LED ได้ ทีนี้ลองถามตัวเอง: ไฟกะพริบช้า ๆ ที่เราทำในคาบแรก กับเสียง square wave วันนี้ ต่างกันแค่ไหน? คำตอบคือ แนวคิดเดียวกัน ต่างแค่ความถี่ — กะพริบช้าพอตาเห็นเป็นไฟ, สลับเร็วพอหูได้ยินเป็นโน้ต นี่คือรากที่ทำให้เสียงกับไฟเป็นเรื่องเดียวกัน
ที่ไป — เสียงนี้จะโตเป็นอะไร ที่สไลด์ "เอาเสียงไปใส่ Pong" เราวางเสียงตาม "เหตุการณ์" ในเกม (บอลชนไม้ → ป๊อก, จบเกม → game over) สังเกตให้ดี: การเก็บทำนองเป็น list แล้ววน for เล่นทีละตัว คือโครงเดียวกับการ ไล่เหตุการณ์ในเกมทีละอัน เสียงจึงไม่ใช่ของแต่งเติมท้ายสุด แต่คือ feedback ที่ผูกกับ game loop คาบหน้าที่เรารวม วาด+จอย+เสียง เป็นเกมเต็มใน full_games/ เสียงจะไปนั่งอยู่ในลูปนั้นพอดี
ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
list ของเลขได้ พรุ่งนี้เราจะไล่ เหตุการณ์ในเกม จาก list แล้วยิงเสียงทีละอันได้ไหม? (ใบ้: for note in melody for event in game)ui.tone / ui.sfx มันคือเรื่องเดียวกับ C engine handoff ในสไลด์ signal chain — เราสั่งหนึ่งบรรทัด งานหนัก (แปลงความถี่ · wavetable · DAC) ข้ามเส้นไปทำในบอร์ด?ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ เสียงกับไฟกับพิกเซล มันคือเรื่องเดียวกัน — แค่เปิด-ปิดกับเลือกจังหวะ" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด เสียงหนึ่งโน้ตวันนี้ไม่ใช่ของเล็ก มันคือชิ้นส่วนของเครื่องเกมทั้งเครื่อง
เทคนิควันนี้ไม่ได้อยู่แค่ในแล็บ มันคือสิ่งที่อุปกรณ์จริงรอบตัวเราใช้กันทุกวัน:
ui.WAVE_SQUARE: ทำนองใน list + รูปคลื่นสี่เหลี่ยมui.sfx(id) แบบเดียวกับที่เราวางเสียงตาม "เหตุการณ์" ใน Pong — feedback ยืนยัน "รับคำสั่งแล้ว"f(n)=440·2^((n−69)/12) — รากฐานเดียวกับที่เราแตะวันนี้เห็นไหมว่าทุกเทคนิคในแล็บวันนี้ ไม่มีอันไหนเป็นของสมมติเลย ตั้งแต่ของเล่นในตู้ ไปจนเครื่องมือแพทย์ ล้วนเปิด-ปิดขาแล้วเลือกรูปคลื่นแบบเดียวกับที่กลุ่มเราเพิ่งทำ
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
time.sleep_ms หยุด ทุกอย่าง ระหว่างโน้ต ถ้าอยากมี BGM เล่นวนพร้อมเกมที่ยังต้องรับปุ่ม จะออกแบบยังไงไม่ให้บล็อกเกม (ใบ้: "จำเวลาที่ผ่านไป" แทนการ sleep — เล่นโน้ตถัดไปเมื่อถึงเวลา)note หรือ velocity กับสถานะ เช่น ยิ่งใกล้แพ้เสียงยิ่งสูง/ถี่ขึ้น จะ map สถานะเป็นพารามิเตอร์เสียงยังไงให้ผู้เล่น "รู้สึก" ตึงเครียดเองโดยไม่ต้องมองคะแนนwave กับชุดโน้ต (scale) ยังไงให้ทุกเสียงเข้าพวกกัน ไม่ใช่ต่างคนต่างมาfull_games/ ลองคิดล่วงหน้า: จะวาง ui.sfx / ui.tone ตรงไหนของ game loop ให้เสียงตรงกับภาพและปุ่ม โดยเสียงยาว ๆ ไม่หน่วงจนเฟรมสะดุด — โจทย์แรกที่เกมเต็มต้องแก้เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากเสียงที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css