จบชุดบทเรียนนี้เราจะเดินครบ 4 เรื่อง แล้วปิดท้ายด้วยการเก็บ dataset จริง:
t_ms เดียวกันs05_multicapture.py ให้เก็บ ≥2 เซนเซอร์บนเวลาเดียว ลงไฟล์ปลายทางของวันนี้: กดปุ่ม label ทำท่า + ส่งเสียงพร้อมกัน แล้วได้ไฟล์ /multicapture.csv ที่ทุกแถวมี IMU และระดับเสียงบน t_ms เดียวกัน
วันนี้เราเน้น "เก็บให้ตรงเวลา" ส่วนการแปลงสัญญาณเป็น feature (FFT / spectrogram) เก็บไว้เป็นเป้าหมายของ โมดูล 4 (Analysis)
บทเรียน 2.1–2.2 เราสร้าง s04_daq_logger.py เก็บ IMU 6 แกนลง CSV ทีละ label ได้แล้ว นั่นคือ เซนเซอร์เดียว บนเส้นเวลาเดียว — วันนี้เราต่อยอดเป็น สองเซนเซอร์
record() · เขียน CSV ทีละแถว · finally เก็บกวาดt_ms ให้ทุกแถวถ้าบทเรียน 2.1–2.2 เข้าหัวแล้ว ชุดบทเรียนนี้คือการ "บวกอีกหนึ่งเซนเซอร์" — เห็นชัดว่า pattern การเก็บข้อมูลขยายได้เรื่อยๆ ไม่ใช่เขียนใหม่ทั้งหมด
โมเดล Edge AI ที่ทรงพลังหลายตัวไม่ได้ดูสัญญาณเดียว มันดู หลายสัญญาณพร้อมกัน แล้วตัดสินใจจากภาพรวม — แต่จะทำแบบนั้นได้ dataset ต้องเก็บมาให้ตรงเวลากันตั้งแต่แรก
หัวใจของชุดบทเรียน: ถ้าเก็บเสียงกับ IMU มา "คนละไฟล์ คนละเวลา" เราจะจับคู่มันทีหลังไม่ได้เลย — ต้องมัดด้วยเส้นเวลาเดียวตั้งแต่ตอนเก็บ
จำเมนู 6 โมเดลชุดบทเรียนแรกได้ไหม — 4 ใน 6 ใช้ไมโครโฟน (Baby Cry / Cough / Alarm / Siren) งานเสียงจึงเป็นสนามใหญ่ และการเก็บเสียงเป็นทักษะ DAQ ที่ขาดไม่ได้
เก็บเสียงบนอุปกรณ์แล้ววิเคราะห์ในเครื่อง = ความเป็นส่วนตัว เสียงไม่ต้องออกไปคลาวด์ — นี่คือจุดขายของ Edge AI ที่เราเห็นตั้งแต่บทเรียน 1.1–1.3
ไมโครโฟนบนบอร์ดเป็นแบบ PDM (Pulse-Density Modulation) มันส่งบิต 0/1 ความเร็วสูงมา ฮาร์ดแวร์แปลงเป็นตัวอย่างเสียง PCM (ตัวเลข 16-bit) ให้เราอ่านเป็น array ได้
from machine import PDM_PCM
pdm = PDM_PCM(0, sck="P8_5", data="P8_6", sample_rate=16000)
buf = array.array("h", (0 for _ in range(512))) # 'h' = 16-bit signed
pdm.readinto(buf) # เติม buf ด้วยตัวอย่างเสียงชุดใหม่
sample_rate=16000คือ 16000 ตัวอย่างต่อวินาที ซึ่งเป็นอัตรามาตรฐานของงานเสียงพูด/เสียงสิ่งแวดล้อม — เพียงพอสำหรับโมเดลเสียงส่วนใหญ่ในคอร์ส
buf เต็มไปด้วยตัวเลขหลายร้อยตัว เราย่อมันเป็น ค่าเดียวต่อเฟรม ที่บอก "ตอนนี้ดังแค่ไหน" — วิธีมาตรฐานคือหา RMS แล้วแปลงเป็น dBFS
def dbfs(chunk):
acc = 0
for s in chunk:
acc += s * s # กำลังสองของแต่ละตัวอย่าง
rms = math.sqrt(acc / len(chunk))
return 20 * math.log10(rms / 32768) if rms > 0 else -96.0
db ต่อแถวทำไมไม่เก็บทุกตัวอย่างลง CSV? เพราะ 16000 ค่า/วินาที จะใหญ่มหาศาล — ในชุดบทเรียนนี้เราเก็บ สรุประดับเสียง ต่อแถวไว้ใน CSV ส่วนเสียงดิบเก็บแยกเป็น WAV (ในฉบับเต็ม)
ไมค์ PDM ถูกแปลงเป็น PCM ที่ อัตราสุ่ม (sample rate) — sample_rate=16000 แปลว่าเก็บ 16,000 ตัวอย่างต่อวินาที ระยะห่างระหว่างสองตัวอย่างจึงคงที่:
array.array("h", ...) ที่เก็บเลข 16-bit เรียงกันตรงๆทำไมเลือก 16 kHz? เสียงพูดและเสียงสิ่งแวดล้อมส่วนใหญ่มีพลังงานต่ำกว่า 8 kHz ตามกฎ Nyquist อัตราสุ่มต้อง สองเท่าของความถี่สูงสุดที่อยากเก็บ 16 kHz จึงพอดีกับงานเสียงในคอร์ส และประหยัดหน่วยความจำกว่า 44.1 kHz ของเพลง
เราไม่ได้อ่านทีละตัวอย่าง แต่อ่านทีละก้อน buf ขนาด CHUNK ตัวอย่าง ก้อนหนึ่งจึงครอบคลุมช่วงเวลา:
ลองแทนค่า CHUNK = 512 (จากโค้ด range(512)):
CHUNK ที่ส่งเข้า array.arraydb หนึ่งค่าเป็นตัวแทน — db แถวนี้คือพลังงานเสียงเฉลี่ยของ 32 ms ช่วงนี้CHUNK ใหญ่ เฟรมยิ่งยาว readinto() ยิ่งรอนานขึ้น จึงเพิ่ม jitter ในลูป ~20 ms ของเรานี่คือเหตุผลเบื้องหลังคำใบ้ "ลด
CHUNKเพื่อลด jitter": ถ้า ยาวใกล้หรือเกินจังหวะแถว (RATE_MS = 20) เฟรมเสียงจะกลืนเวลาจนแถวห่างเกินเป้า — และเพราะเราเก็บt_msจริงไว้ทุกแถว เราจึง "เห็น" อาการนี้ย้อนหลังได้
ก่อนเขียนเอง รันตัวอย่าง 06_mic_level_meter.py ให้เห็นเสียงกลายเป็นแถบระดับสดๆ — นี่คือครึ่งแรกของสิ่งที่เราจะเอาไปมัดรวม
pdm.readinto(buf)
rms = math.sqrt(sum(s*s for s in buf) / len(buf))
db = 20 * math.log10(rms / 32768) if rms > 0 else -96.0
bar.value(max(0, int(db + 60))) # แกน 0..60 = -60..0 dBFS
seg.text("%d" % int(db))
readinto -> dbfs -> เอาค่าไปใช้ต่อ"รันของที่ทำงานได้ก่อน" — พอเห็นแถบ VU ขยับตามเสียงตัวเอง คุณจะเข้าใจ
dbfs()โดยไม่ต้องท่องสูตร แล้วค่อยเอาไปประกอบเป็นตัวเก็บ dataset
ตัวอย่าง 07_mic_record_wav.py เก็บ เสียงดิบทั้งก้อน ลงไฟล์ .wav — นี่คืออีกครึ่งที่ฉบับเต็มของเราจะใช้เก็บคลื่นเสียงคู่กับ CSV
with open("/rec.wav", "wb") as f:
f.write(wav_header(total, RATE)) # หัวไฟล์ WAV
while written < total:
pdm.readinto(buf)
f.write(buf) # ต่อท้ายเสียงดิบทีละก้อน
written += CHUNK
mpremote เอาไปฟัง/วิเคราะห์บน PC ได้สองตัวอย่างนี้คือ "วัตถุดิบ" ของชุดบทเรียน: ตัวหนึ่งย่อเสียงเป็นตัวเลข (VU) อีกตัวเก็บเสียงดิบ (WAV) เราจะหยิบทั้งคู่มาต่อกับ IMU บนเส้นเวลาเดียว
ถ้าเก็บเสียงกับ IMU แยกกัน แต่ละตัวมีจังหวะของมันเอง เวลาเอามารวม เราจะไม่รู้ว่า "ค่าเสียงตัวนี้เกิดพร้อมกับการขยับตัวไหน" — ข้อมูลจับคู่ไม่ได้
นี่เป็นปัญหาคลาสสิกของงาน DAQ: ไม่ใช่แค่ "เก็บให้ได้" แต่ "เก็บให้ตรงเวลา" ต่างหากที่ทำให้ dataset ใช้งานได้จริง
วิธีของเราเรียบง่ายและได้ผล: ในหนึ่งรอบของลูป อ่าน IMU และ MIC ติดกัน แล้วเขียนหนึ่งแถวที่ทั้งคู่แชร์ค่าเวลา t_ms เดียวกัน
db กับค่า IMU นี้เกิดในช่วงเวลาเดียวกัน (t_ms)"นี่คือแก่นของชุดบทเรียน ถ้าจำได้เรื่องเดียว ให้จำว่า "อ่านพร้อมกัน เขียนแถวเดียว ประทับเวลาร่วม"
MicroPython ให้นาฬิกามิลลิวินาทีมา เราจับเวลาเริ่มไว้ที่ t0 แล้วทุกแถวเก็บ "ผ่านไปกี่ ms จาก t0"
t0 = time.ticks_ms() # จุดศูนย์ของชุดนี้
# ... ในลูป:
t_ms = time.ticks_diff(time.ticks_ms(), t0) # เวลาผ่านไปเทียบ t0
time.ticks_diff(a, b) แทนการลบตรงๆ (a - b) เพราะตัวนับ ms มี wrap-around (วนกลับ 0) ticks_diff จัดการให้ถูกt_ms เริ่มที่ ~0 ในแถวแรก แล้วเพิ่มขึ้นเรื่อยๆ — เป็นแกนเวลาของทั้งชุดt_ms ค่าเดียวกัน จึงถือว่า "เวลาเดียวกัน"อย่าใช้
time.time()หรือเลขรอบมานับเวลา —ticks_ms/ticks_diffคือคู่มาตรฐานของ MicroPython สำหรับวัดช่วงเวลาสั้นๆ อย่างแม่นและปลอดภัยจาก wrap
เราตั้งใจให้แต่ละแถวห่างกัน RATE_MS = 20 (50 Hz) ด้วย time.sleep_ms(20) แต่ในความจริงงานอ่านเสียง/เขียนไฟล์ก็กินเวลา ทำให้ช่วงจริงเพี้ยนได้เล็กน้อย — ค่านี้เรียก jitter
t_ms จริงไว้ทุกแถว เราจึง ตรวจสอบย้อนหลังได้ ว่าช่วงจริงใกล้ 20 ms แค่ไหนdt เฉลี่ยจริงและ jitter (ช่องว่างที่ใหญ่สุด) แล้วโชว์บนจอ — ถ้า jitter สูงมาก แปลว่า dataset ช่วงนั้นเชื่อถือไม่ได้CHUNK เสียง, ลดงานในลูป, อย่าพิมพ์ console ถี่บทเรียน DAQ: การเก็บ "เวลาจริง" ไว้ ไม่ใช่แค่สมมติว่าตรง 20 ms เป๊ะ ทำให้เราตรวจคุณภาพ dataset ได้ — วิศวกรที่ดีไม่เชื่อ ตรวจเสมอ
ไฟล์ที่เราจะได้หน้าตาแบบนี้ หนึ่งบรรทัดต่อหนึ่งแถวเวลา ทุกคอลัมน์ของแถวเดียวกันอ้าง t_ms เดียวกัน
t_ms,label,ax,ay,az,gx,gy,gz,db
0,shaking,0.02,-0.98,0.11,1.4,-0.7,0.3,-41.2
20,shaking,0.31,-0.88,0.25,8.9,-3.1,1.2,-38.5
40,shaking,-0.12,-1.02,0.07,-5.2,2.4,-0.9,-37.9
| คอลัมน์ | มาจาก | ความหมาย |
|---|---|---|
t_ms |
ticks_diff |
เส้นเวลาร่วม (ms จากต้นชุด) |
label |
ปุ่มที่กด | ท่า/คลาสของชุดนี้ (idle/circle/shaking) |
ax..gz |
bmi270.motion() |
เซนเซอร์ 1 — accel 3 แกน + gyro 3 แกน |
db |
dbfs(buf) |
เซนเซอร์ 2 — ระดับเสียงของเฟรมนั้น |
สังเกตว่า
labelอยู่ในทุกแถว — เราเก็บไปพร้อมข้อมูลเลย จะได้ไม่ต้องมานั่งใส่ label ทีหลัง นี่คือ dataset ที่ "พร้อม train" ตั้งแต่ออกจากบอร์ด