ChucK-programming

สวัสดีครับ มาแนะนำขั้นตอนสั้นๆ สำหรับการลงโปรแกรม miniAudicle ที่เป็น editor และตัว รัน ภาษา ChucK programming language

miniAudicle: http://audicle.cs.princeton.edu/mini/
ChucK: http://chuck.cs.princeton.edu/

ตอนนี้ผมใช้ Ubuntu Jaunty (9.04) แต่ว่า สำหรับเวอร์ชั่นล่าสุดๆ ก็ทำตามนี้

เนื่องจาก ChucK ตอนนี้อยู่ที่เวอร์ชั่น 1.2.1.2 แต่ source code ของ miniAudicle ดันใช้ ChucK 1.2.1.1 ดังนั้นจึงต้องมีการพลิกแพลงอะไรกันนิดหน่อย จะได้เป็น miniAudicle ที่มี ChucK ล่าสุด

สิ่งที่ต้องการสำหรับ compile:
ให้ลง package เหล่านี้ก่อน
build-essential bison flex libsndfile1-dev libwxgtk2.6-dev

สำหรับจะใช้ miniAudicle กับ jack ให้ลง  libjack-dev
สำหรับจะใช้กับ alsa ให้ลง  libasound2-dev 

แต่สำหรับผมผม compile ไว้สำหรับทั้งสองเวอร์ชั่น ก็ลงทั้งสอง ทั้งสำหรับ jack, alsa ไปเลย

 เมื่อได้  source code ของ miniAudicle สำหรับ linux มาแล้ว ให้ extract ไฟล์นั้นออกมาก่อน จะเจอ folder ชื่อ chuck-1.2.1.1 ลบไฟล์ที่อยู่ในนั้นให้หมด แล้ว พักไว้ก่อน

หลังจากนั้น เราโหลด source code ของ ChucK เวอร์ชั่น 1.2.1.2 สำหรับ linux มาแล้ว ให้ extract ลงไปใน folder ข้างบน (ไอ้อัน chuck-1.2.1.1) ไปเลย

กรณี compile สำหรับ alsa ใน terminal ให้ไปที่ folder ที่เราแตก miniAudicle source code ไว้ แล้วพิมพ์ make linux-alsa พอมัน build เสร็จ จะเกิด executable ขึ้นไฟล์เดียวใน folder ชื่อ wxw ชื่อไฟล์ miniAudicle ให้ทำการ copy ไปไว้ที่ /usr/bin

กรณี compile สำหรับ jack ให้ใช้คำสั่ง make linux-jack แทน แต่ผมมักจะเปลี่ยนชื่อ wxw/miniAudicle เป็น miniAudicle-jack แล้วย้ายไปไว้ใน /usr/bin เช่นกัน

หมายเหตุ: ถ้า make สำหรับ alsa และ jack เพื่อความสะดวก ควรจะลบ source code ทั้งหมดทิ้ง แล้ว แตกออกมาใหม่ ตามวิธีข้างบน แล้วค่อย make ใหม่

หลังจากนั้น เมื่อเราอยากรัน miniAudicle หรือ miniAudicle-jack ก็สามารถ run จากที่ไหนก็ได้เลยครับ 

Octave
Note Numbers
CC#D D#EFF#GG#AA#B
-1 01 2 3 45 67891011
0121314 1516171819202122 23
1242526272829303132333435
2363738394041424344454647
348495051525354 5556575859
4606162636465666768697071
5727374757677787980818283
6848586878889909192939495
796979899100101102103104105106107
8108109110111112113114115116117118119
9120121122123124125 126127
เรามาเริ่มทำอะไรที่ดูเกี่ยวกับดนตรีมากขึ้นกันดีกว่า หลังจากที่ผ่านมา เจอเน่านี้ นำเสนอด้านการเขียนโปรแกรมมาหลายทีละ อิอิ
(แต่สิ่งที่จะทำให้ดูต่อไปนี่ ก็อาศัยผสมผสานประสบการณ์ที่ผ่านๆมาใน เจอเน่านะครับ
ถ้าใครอ่านแล้วงง อาจจะเป็นเพราะว่า ยังไม่ได้ลองทำอันที่ผ่านๆมาเล่น)

แล้วนี่เอาตารางอะไรของชาวบ้านเขาขึ้นมาเนี่ย
จะดูดวงกันหรือไง? อ้อ ไม่ใช่
นี่คือ ตาราง MIDI Note Numbers!
http://www.harmony-central.com/MIDI/Doc/table2.html

จากระบบการส่งข้อมูลของ midi
ก็ได้เกิดมาตรฐานที่เป็นที่นิยมกัน
ในการส่งค่าระดับเสียง โน้ต ในดนตรีสากล
ระดับเสียงที่ว่านี้ ก็เทียบตามโน้ตในดนตรีสากลโดยตรงเลย
ก็คือ โด่ เร มี้ ฯลฯ
โดยไล่ ออคเทฟ (octave) ต่างๆ
ตั้งแต่ ต่ำกว่าเปียโน จนไปถึงสูงกว่าเปียโน

ด้วยความที่ ยุคนั้น ค่าของ midi ยังรับส่งกันด้วยข้อมูล 7 bit แบบนี้
(สมัยนี้ก็เห็นยังใช้อยู่นะ ฮ่าๆๆ)
ทำให้ ส่งความละเอียดได้เพียง 27 นั่นคือ 128 หน่วย
ถ้าคิดเป็นโน้ต ก็คือ มันครอบคลุมจำนวนโน้ตที่กว้างมาก
กว้างกว่าเปียโนหน่อยๆ ก็ถือว่า ใช้ทำงานดนตรีได้กว้างพอควรเลยล่ะ

สิ่งที่ควรรู้ก็คือ:
ระบบดนตรีสากลดั้งเดิมเนี่ย
จะแบ่งระดับเสียงต่างๆ เป็น octaveๆ
เวลาลองไล่โน้ตครบชุดหนึ่ง
เมื่อโน้ตไล่แล้ววนกลับมาเป็นโน้ตตัวเดิม
(แต่มีระดับเสียงต่างกัน)
ก็จะเรียกว่า เป็นโน้ตเดียวกันแต่อยู่ต่าง octave กัน

เช่น โด่ โด โด๊

-_-"

คราวนี้ มีตัว โด (C) ตัวหนึ่ง เป็น C ที่มีจุดเด่นอยู่ตรงที่
เป็นโน้ตที่เป็นเสียงที่อยู่ตรงกลางระหว่างเสียงผู้ชายกับผู้หญิง
(แล้วก็เป็น C ที่อยู่ กลางๆ ค่อนไปทางซ้ายนิดๆ ของ piano)
โน้ตตัวนี้ เรียกว่า middle C (C กลาง)
ในระบบ midi ก็ได้กำหนดว่า โน้ตหมายเลข 60 นั้น ให้เป็น C กลาง

หลังจากนั้น โน้ตอื่นๆ ก็จะคิดเทียบกับ ซี กลาง ได้เลย

คราวนี้ อีกหนึ่งอย่างที่ควรรู้จักก็คือ tone และ semitone
โทน ก็คือโทนเสียง และการเทียบเสียง ตามปกติที่เรารู้จัก
เช่น ถ้าเราเคยออกเสียงร้อง โด เร มี ฟา ..
การเดินทางจาก โด ไป เร.. นี่ก็คือ เพิ่มขึ้น 1 โทนเสียง
เร ไป มี ก็เพิ่มขึ้น 2 โทนเสียง..
เป็นเช่นนี้ไปเรื่อยๆ
แต่ถ้า ลองคิดเสียงในหัวดู (โดยเฉพาะคนที่รู้จัก major scale)
จะสังเกตได้ว่า เสียงที่เพิ่มขึ้นไป ในแต่ละโทนเสียง มันไม่ได้ขึ้นไปเท่ากัน
บางที มันก็เหมือนขึ้นไป 1 โทนเต็มๆ
บางที ก็เหมือนเดินขึ้นไปแค่ ครึ่งโทนเสียงเท่านั้น
เช่น โด ไปเร นี่ เพิ่ม 1 โทนเสียง
แต่ลองออกเสียง มี ไปฟา นี่รู้สึกได้ว่า เพิ่มขึ้นไปแค่ ครึ่งเท่านั้น)

ศัพท์ที่ว่า "ครึ่งโทนเสียง" ก็เรียกว่า semitone (เซมิโทน .. เซมิ = ครึ่ง)
ในดนตรีสากลปกติที่เรารู้จัก ระดับเสียงที่ห่างกัน 1 octave นั้น
ห่างกัน 12 semitone ด้วยกัน
ดูจากตารางแล้วก็จะเห็นสอดคล่้องกัน ก็คือ
เมื่อโน้ตขึ้น octave ใหม่ ก็จะวนทีละ 12 ตัว


ตอนนี้เรามาทดลองฟังเสียงกันดีกว่า
เริ่มจาก UGen (unit generator) ที่เรารู้จักแล้วก็คือ
SinOsc.. (sine wave oscillator)

พิมพ์โค้ดดังนี้ครับ

SinOsc s1 => dac;
2::second => now;

เสร็จแล้วลอง Add Shred แล้วฟังดู
อันนี้ก็เป็นการฟังเสียง Sine Wave อย่างง่ายๆ
แต่ว่าจะเห็นว่า เขาปรับความถี่ไว้แล้ว
แต่เป็นเท่าไหร่ล่ะ?

การที่เราจะ อ่าน (read) หรือ ปรับ (write) ค่าความถี่ของ oscillator หนึ่งๆนั้น
มีฟังก์ชั่นหนึ่งๆ ที่ทำงานอย่างนี้ของ oscillator ต่างๆ
ก็คือ freq()
ถ้าเรียก freq() ตรงๆเลย ก็จะเป็นการ อ่านค่า
อ่านแล้วก็นำมาใช้งานต่างๆ (เช่นคำนวณ..) หรือนำมาพิมพ์ออกจอก็ได้ เช่น

SinOsc s1 => dac;
<<< s1.freq() >>>;
2::second => now;

จะเห็นว่า เขาพิมพ์ค่าออกมาทาง Console ว่า 220.0 เป็นค่าชนิด float
ก็แปลว่า ความถี่ของ s1 ในขณะนั้น เป็น 220hz

ถ้าใครคุ้นๆ อาจจะเคยได้ยินกว่า A 440hz
โน้ต A ที่เป็นโน้ตที่ชอบใช้จูนเครื่องดนตรีคลาสสิค ทั่วไป
(เช่น.. ไวโอลิน สายเปล่า สาย 2..)
แล้ว 220hz นี่มันโน้ตอะไรกัน
อ๋อ มันมีค่าเพียงแค่ ครึ่งเดียวของ A 440hz
งั้นมันก็คือ A 220hz พอดี..
โน้ตที่มีความถี่ครึ่งเดียว ก็คือโน้ตเดียวกัน ที่ต่ำลงมา 1 octave
ในทางกลับกัน
โน้ตที่มีความถี่เป็นสองเท่า ก็คือโน้ตเดียวกัน ที่สูงขึ้นไป 1 octave

(ก็แปลว่า ถ้าจะหา A ที่สูงกว่า A 440hz ไป 1 octave ก็จะมีความถี่ 440x2 = 880hz)

คราวนี้ ลองหันไปดูตารางข้างบนกัน
เราจะเห็นว่า C กลาง.. มีค่า midi note number = 60
ถ้ามองแถวๆนั้น จะเห็น A ที่สูงกว่า C กลาง และต่ำกว่า C กลาง
ก็คือมีค่า 69 และ 57 ตามลำดับ

ลองพิมพ์โค้ดตามนี้เลย

SinOsc s1 => dac;
57 => int noteNumber;
s1.freq(Std.mtof(noteNumber));
1::second => now;
69 => noteNumber;
s1.freq(Std.mtof(noteNumber));
1::second => now;

ลองทำแล้วฟังเสียงดูครับ
แล้วลองทำแบบนี้
ทำการพิมพ์ค่าของ s1.freq() ออกมา ในช่วงบรรทัดต่างๆ
จะเห็นว่า ข้างบนนี่ เราได้ทำการเปลี่ยน ความถี่ของ s1 ให้กลายเป็นค่าใหม่
แต่ค่าใหม่นี้ เราใช้ ฟังก์ชั่นหนึ่ง คือ Std.mtof()

ในภาษา ชัก มีฟังก์ชั่นมาตรฐานในการทำงานต่างๆมากมาย
ซึ่งอยู่ใน Std นี่แหละ (standard)
อย่างเช่น Std.mtof คู่มืออธิบายดังนี้

[function]: float mtof ( float value );

  • converts a MIDI note number to frequency (Hz)
  • note the input value is of type 'float' (supports fractional note number)
ถ้าอ่านแล้วก็จะแปลว่า เป็นฟังก์ชั่น ที่ช่วยเปลี่ยนค่า midi note เป็นความถี่
mtof สามารถรับค่าเป็น float ได้ด้วย
(รับค่า midi note ที่ไม่เป็นจำนวนเต็มได้ เช่นเราอยากรู้ความถี่ของ โน้ตที่
อยู่กึ่งๆระหว่าง C และ C# ก็ยังได้)
ที่เขาเขียนว่า float mtof ( float value)

float ตัวแรก หมายถึง mtof นี่ จะตอบค่าออกมาเป็นเลขชนิด float
และ float ตัวหลัง หมายถึง มันจะรับค่าที่จะนำไปคำนวณชนิด float

ลองแบบนี้กันเลยก็ได้
<<< Std.mtof(57) >>>;

จะเห็นว่า ที่ Console แสดงค่าความถี่ของ A ตัวที่ต่ำว่า C กลางขึ้นมาทันที
ก็คือ ความถี่ 220hz แต่แสดงเป็น 220.0 (เป็นค่าชนิด float)


ให้ลองทำ:
ลองใช้ Std.mtof()
ในการปรับ SinOsc ให้เป็นโน้ตตามตาราง
แล้วอาศัยการ ชัก เวลา (เช่น 0.5::second, 1::second ฯลฯ) ใส่ now
ในการปรับความยาวของโน้ตต่างๆ
แล้วทำออกมาเป็นเพลงฮะ


ตัวอย่าง:
SinOsc s1 => dac;
Std.mtof(64) => s1.freq; second => now;
Std.mtof(60) => s1.freq; second => now;
Std.mtof(62) => s1.freq; second => now;
Std.mtof(55) => s1.freq; 1.5::second => now;
s1.gain(0.0); .5::second => now;

s1.gain(1.0);
Std.mtof(55) => s1.freq; second => now;
Std.mtof(62) => s1.freq; 1.25::second => now;
Std.mtof(64) => s1.freq; 1.5::second => now;
Std.mtof(64) => s1.freq; 2::second => now;
s1.gain(0.0); .5::second => now;

s1.gain(1.0);
Std.mtof(48) => s1.freq; 2::second => now;

หมายเหตุ:
การ => ใส่ฟังก์ชั่น มีความหมายเดียวกับการส่งค่านั้นใน function
ดังนั้น การ (สิ่งที่ต้องการ) => s1.freq มีความหมายเท่ากับ s1.freq(สิ่งที่ต้องการ)

gain ก็เป็น function ของ SinOsc เช่นเดียวกับ freq
ตัว freq นั้น เป็นฟังก์ชั่นสำหรับ อ่าน และ ปรับค่า ความถี่
ส่วน gain นั้น อ๋อ ถ้าให้เดาแล้ว ไม่ยากเลย มันก็คือ ความดังน่ะแหละ
0.0 ก็คือ เงียบสนิท, 1.0 ก็คือดัง 100% ดังสุดแล้ว

วันนี้เราจะมาว่ากันด้วยเรื่องการของการทำอะไร ซ้ำๆ ใน ChucK นะครับ


เท่าที่ผ่านมาที่พวกเราทดลองชักเล่นกันเนี่ย เอ๊ย ผิดๆ เล่นชัก
ลักษณะการทำงานของโปรแกรมต่างๆที่เขียน
ก็จะทำงานเรียงลำดับ จากบรรทัด บน ลงมาบรรทัดล่าง ไปเรื่อยๆ
เหมือนเวลาเราอ่านคำอธิบายอะไรซักอย่างแล้วทำตามเป็นขั้นๆ

แต่ถ้า เราอยากทำอะไรซ้ำๆล่ะ?
คนที่เขียนเขียน C, C++, Java ก็คงพูดเลยว่า อ๋อๆ มีวิธีการทำให้อะไรมันทำงานซ้ำๆกัน ก็คือ การวนลูป นี่เอง
การวนลูปนี่ก็มีหลายอย่าง แต่ปกติแล้ว การวน ก็จะวนแบบ มีเงื่อนไข
ที่บอกว่ามีเงื่อนไขก็คือ จะทำการวนก็ต่อเมื่อ อะไรบางอย่างมันเป็นจริง
(หรือในทางกลับกันก็คือ จะหยุดวน ก็ต่อเมื่อ อะไรบางอย่างไม่เป็นจริง)

ก่อนที่เรากำลังจะทำการวน ผมจะแนะนำให้รู้จักสัญลักษณ์อย่างหนึ่ง
ที่ผ่านมา เราอาจจะเคยเห็น วงเล็บ แล้วใช้ในการคำนวณ
เช่น.. (1 + 2) * 3 แปลว่า เอา 1 ไปบวก 2 ก่อน (ได้ 3) แล้วค่อยคูณด้วย 3 (ได้ 9)
แต่ 1 + 2 * 3 แปลว่า เอา 2 ไปคูณ 3 ก่อน (ได้ 6) แล้วค่อยบวกกับ 1 (เพราะการ บวก ให้ทำทีหลังคูณ)

ก็คือ วงเล็บนี่ เป็นตัวบังคับว่า ให้ทำอะไรก่อน หลัง

แต่ในการสั่งคำสั่งบรรทัดต่างๆ
เราสามารถกำหนดให้คำสั่งต่างๆ รวมกันเป็นเสมือนประโยคเดียวกัน หรือเรียกว่า เป็น expression เดียวกัน

expression นี่ ก็คือ พวกประโยคต่างๆที่มีการคำนวณ มีค่า อะไรก็แล้วแต่ แต่ทำให้เกิดการคำนวณขึ้นมา
เช่น 1 ก็เป็น expression
1 + 2 ก็เป็น expression
SinOsc s => dac
ก็เป็น expression ชนิดหนึ่ง
expression ต่างๆ จะจบด้วยเครื่องหมาย ;
คราวนี้ ถ้าเราอยากเขียนเสมือน หนึ่ง expression
แต่อยากเขียนหลายๆ expression เรียงต่อกันล่ะ?

ก็ต้องใช้ วงเล็บปีกกา หรือ { } ในการจัดกลุ่ม expression เหล่านั้น
ให้รวมเสมือนเป็นหนึ่ง expression

โดยมีกฎเกณฑ์อย่างหนึ่งก็คือ สิ่งที่อยู่ภายใน { } เดียวกัน สามารถประกาศ ตัวแปร (และสิ่งอื่นๆ เช่น ฟังก์ชั่น, คลาส) ของมันเองได้
แต่เมื่อออกไปนอก { } แล้ว หรือเสร็จจากการทำงานใน { } นั้นๆแล้ว
ตัวแปรเหล่านั้น ก็จะไม่มีอีกต่อไป

มันเหมือนกับว่า เป็นตัวแปรส่วนตัวของขอบเขตนั้นๆ (เรียกว่าเป็น local)

เอ้า เราลองมาเขียนโปรแกรมนี้กันดู
ไหนๆก็ใกล้จะง่วงแล้ว ก็เอารูปมาให้ดูกัน จะได้ตื่นขึ้นมา อาห์..



เอ้า! พิมพ์ตามได้เลย

อะไรกัน! ขี้เกียจพิมพ์เหรอ งั้นเอานี่ไปกิน!

1 => int a;
2.0 => float b;
2::second => dur c;
<<< "outside { }" >>>;
<<< a >>>;
<<< b >>>;
<<< c >>>;

{
<<< "inside { }" >>>;
<<< a >>>;
<<< b >>>;
<<< c >>>;
 
<<< "initialize a and b:" >>>;
3 => int a;
4 => int b;
<<< a >>>;
<<< b >>>;
<<< c >>>;
}

<<< "outside { } (We're back!)" >>>;
<<< a >>>;
<<< b >>>;
<<< c >>>;

พอรันแล้วได้ผลดังนี้

"outside { }" : (string)
1 :(int)
2.000000 :(float)
88200.000000 :(dur)
"inside { }" : (string)
1 :(int)
2.000000 :(float)
88200.000000 :(dur)
"initialize a and b:" : (string)
3 :(int)
4 :(int)
88200.000000 :(dur)
"outside { } (We're back!)" : (string)
1 :(int)
2.000000 :(float)
88200.000000 :(dur)

เอามาลองวิเคราะห์ดูเล่นๆอ่ะ

ตอนแรกพวกเราสร้าง ตัวแปรขึ้นสามประเภท
เป็นชื่อ a, b, c ตามลำดับ
ตัวแรกเป็น integer (int), ตัวที่สองเป็น float, ตัวที่สามเป็น duration (dur)
ฮั่นแน่ อันนี้ยังไม่เคยเห็นละเซ่ เอามาโชว์ก่อน
duration แปลว่า "ระยะเวลา"
อย่างเช่น "ระยะเวลา 2 วินาที" ที่เรากำลังให้ C ไง
แต่ข้ามเรื่องนี้ไปก่อน (เอามาให้ดูรู้จักกันไว้ก่อน วันหลังเผื่อเป็นเพื่อนกัน อิอิ)
(หรือเป็นกิ๊กดีนะ!!)


ตอนแรก เราอยู่นอก { } (ก่อนจะเข้า { .. )
มันก็สามารถพิมพ์ ค่า a, b, c ตามลำดับได้ตามปกติ
แถมพิมพ์ค่า dur ออกมาซะสวยหรูเลย เป็นเลข 88200 (ไว้จะอธิบายอีกที)

แต่แล้ว เราก็เข้าไปใน { }
ซึ่งในนั้นน่ะ จริงๆแล้ว เขาก็รู้จัก a, b, c ที่พวกเรามีอยู่
แต่ทันใดนั้น ภายใน { } นั้นเอง ก็ได้มีการกำหนด a, b, c ขึ้นมาใหม่
แถมยังเป็นตัวแปรคนละชนิดกันเสียด้วย!
แล้วเมื่อแสดงค่าออกมา จะเห็นว่า a, b ที่ถูก ประกาศ ออกมาใหม่นั้น
ได้เปลี่ยนไปเป็นคนอื่นไปซะแล้ว!!

แต่ไม่ต้องตกใจ เมื่ออกจาก { } (จบ } .. ออกมา)
พอลองพิมพ์ค่า a, b, c
ค่า a, b, c ดั้งเดิม ก็ยังอยู่เหมือนเดิมไม่เปลี่ยนแปลง
คล้ายๆกับว่า ได้หมดภารกิจ ของ a, b ใน { } อันนั้นแล้ว
ก็ปลดประจำการไป


หวังว่า จะเข้าใจเรื่องการใช้ { } กันมาหนึ่งระดับละ
ต่อไปเราจะมาชักซ้ำๆกัน


เริ่มมาจากคำสั่งกลุ่มแรกก่อน
"ทำ"

ฮ่าๆๆ หมายถึงคำว่า do นี่แหละ
เวลาเราอยากสั่งให้ใครทำอะไรซ้ำๆ
ก็บอกว่า
ทำ (สิ่งที่อยากจะให้ทำ) ในขณะที่ (สิ่งที่จะทดสอบว่าจริงหรือไม่)

รูปแบบของการใช้ do ก็คือ
do { สิ่งที่สั่งให้มันทำ }
while (เงื่อนไข .. ถ้าเป็นจริง มันจะยังทำซ้ำอีก);

เช่น เราอยากสั่งเครื่องให้ "เพิ่มค่า i ไปเรื่อยๆ ทีละ 1" ในขณะที่ "i น้อยกว่า 10"
ลองพิมพ์โปรแกรมตามนี้เลย

0 => int i;
<<< "i starts from", i >>>;
do i + 1 => i;
while (i < 10);
<<< "now i =", i >>>;

ได้ผลลัพท์ออกมาดังนี้:
i starts from 0
now i = 10

โห อะไรจะเรียบง่ายขนาดนั้น
สิ่งที่เกิดขึ้นก็คือ ตอนแรก i เท่ากับ 0 อยู่ดีๆ
เข้าไปใน ลูปชนิด do ก็เลยต้องทำตามที่เขาสั่ง
ก็คือสั่งให้เอาค่า i ไปบวกด้วย 1 แล้ว ชัก เก็บเข้าไปไว้ที่ i เช่นเดิม
(มันก็เหมือนกับการ เพิ่ม i ขึ้นทีละ 1 นี่แหละ)
แต่มีเงื่อนไขว่า ให้ทำไปเรื่อยๆ ในขณะที่ i < 10
ดังนั้น พอครั้งที่ i เป็น 9
แล้วโดนเพิ่มเป็น 10
พอทดสอบว่า while (i < 10);
ไอ้ตอนนั้นเนี่ย i มันดัน ไม่น้อยกว่า 10 แล้ว มันดันเท่ากับ 10 พอดี
การทำงานก็เลย ออกมาจาก วังวนนั้น
ค่า i ก็เลยเป็น 10 อยู่ เห็นภาพแล้วบ่ ? งั้นมาทำภาพให้ดูชัดๆกว่านี้
แก้โค้ดเป็นนี่เลยครับ

0 => int i;
<<< "i starts from", i >>>;
do
{
i + 1 => i;
<<< "now i =", i >>>;
}
while (i < 10);
<<< "finally.. i =", i >>>;

ได้ผลลัพท์ออกมาเป็น
i starts from 0
now i = 1
now i = 2
now i = 3
now i = 4
now i = 5
now i = 6
now i = 7
now i = 8
now i = 9
now i = 10
finally.. i = 10

นี่เลย ตอนนี้ แทนที่เราจะสั่งให้โปรแกรมมัน do แค่คำสั่งเดียว
ก็จัดการสั่งมันเป็นชุดเลย คือทั้ง เพิ่มค่า i แล้วยังจะ พิมพ์ออกมาในแต่ละรอบอีกด้วยแหนะ

จุดเด่นของ do ก็คือ เมื่อเริ่มต้นมาเนี่ย
ไม่ได้มีการทดสอบใดๆเลย เริ่มต้นมาก็ต้อง ทำตามที่มันกำหนดใน do ก่อน
แต่ถ้าเงื่อนไขยังถูกต้องตาม while ก็ ทำซ้ำต่อไป
ถ้าเงื่อนไขไม่ถูกต้องแล้ว ก็หลุดออกมาจาก while
ดังนั้น .. ย้ำ ครับ .. เข้าไปใน do สิ่งที่กำหนดไว้หลัง do จะถูกกระทำก่อนที่จะมีการทดสอบด้วย while

แต่ถ้าเราอยากทดสอบก่อนเลยล่ะ?
แบบว่า ถ้าทดสอบแล้วไม่ได้ตามเงื่อนไข
ก็ไม่ต้องไปทำตามมันเลย

แบบนี้ เขาให้ใช้ while นี่แหละครับ เรียกว่าเป็น ลูป ชนิด while
รูปแบบการใช้ while มีลักษณะแบบนี้
while (เงื่อนไข .. ถ้าเป็นจริง มันจะให้ทำสิ่งต่อไปนี้..) { สิ่งที่จะให้ทำ }

สมมติ ว่าเรามีลูกเต๋า 2 อัน (ลูกเต๋า 6 หน้า.. มีเลขตั้งแต่ 1 .. 6)
เราอยากจะสร้างค่าที่มาจากลูกเต๋าสองอัน
แต่มีเงื่อนไขว่า ลูกเต๋าสองอัน ห้ามเป็นเลขซ้ำกัน
อันนี้ผมจะแนะนำให้รู้จัก ฟังก์ชั่นมาตรฐานหนึ่งใน ChucK ด้วย
เขาชื่อ Std.rand2()
(Standard Function: Random สำหรับ ระหว่าง int 2 ตัว)

เราสามารถสร้างค่าสุ่มได้จากฟังก์ชั่นนี้
โดยการกำหนดว่า ค่าที่สุ่มนั้น อยู่ในช่วงเลขจำนวนเต็มเท่าไหร่ถึงเท่าไหร่
เรามาลองดูโปรแกรมสั้นๆนี่ก่อน

Std.rand2(1, 6) => int dice1;
<<< dice1 >>>;

ลองรันดูครับ (อ้อ รัน นี่ผมหมายถึง Add Shred)
แล้วก็ รันหลายๆครั้งด้วย แล้วลองดูค่าใน Console Monitor
จะเห็นว่า มันขึ้นมาอย่างกับทอยลูกเต๋าจริงๆเลยล่ะ
เป็นอย่างที่พวกเราคาดไว้
งั้นต่อไป พวกเรามาลอง ทอยลูกเต๋าสองอัน

Std.rand2(1, 6) => int dice1;
Std.rand2(1, 6) => int dice2;
<<< dice1, dice2 >>>;
if (dice1 == dice2) <<< "Both dice has the same number." >>>;

ลองรันไปเรื่อยๆครับ หลายๆที
น่าระทึกใจจริงๆ

จะเห็นว่า ลูกต๋าสองอัน มันก็ออกมาสุ่มไปเรื่อยๆของมัน
แต่พอครั้งที่เลขหน้าตรงกัน ก็จะพิมพ์ขึ้นมาด้วยว่า
หน้าสองอันมันเหมือนกันโว้ย

(ประโยค if (เงื่อนไขที่จะทดสอบ) { สิ่งที่จะทำเมื่อเป็นจริง } ผมก็เลย แอบเอามาแนะนำในที่นี่ด้วยฮ่าๆ
อ่านแล้วก็เข้าใจไม่ยากหรอกน่า อ่านง่ายๆ
ถ้างง ก็คงจะเครื่องหมาย == เครื่องหมายนี้ เอาไว้ทดสอบว่า เท่ากัน หรือเปล่า)

แล้วทำยังไงล่ะ ถึงจะแน่ใจได้ว่า ลูกเต๋าอีกอัน ต้องออกหน้าไม่ตรงกับของเดิม?
เรามาทดสอบกันดีกว่า

Std.rand2(1, 6) => int dice1;
Std.rand2(1, 6) => int dice2;
while(dice1 != dice2) Std.rand2(1, 6) => dice2;
<<< dice1, dice2 >>>;

คราวนี้ ไม่ว่าจะ add shred เท่าไหร่
ลูกเต๋าก็ไม่มีหน้าเท่ากันอีกแล้ว
เพราะถ้าหน้าเท่ากันเมื่อไหร่ มันจะทำการทอยลูกเต๋าใหม่ไปเรื่อยๆ
จนกว่า จะเจอครั้งที่หน้าไม่เท่ากัน

อีกอย่างที่จะนำเสนอในวันนี้ ก็คือ ลูปที่ชื่อ for

for นี่จะทำงานสี่อย่างด้วยการ
1. กำหนดการกระทำเริ่มต้น (เตรียมตัวเข้าลูป)
2. ทำงานที่กำหนดให้กระทำ
3. ดำเนินการเปลี่ยนค่านแต่ละรอบ
4. ทดสอบเงื่อนไขว่าเป็นจริงหรือไม่ ถ้าจริงให้วนต่อไป ไม่จริงก็จบการทำงาน

มีรูปแบบดังนี้
for (เตรียมตัว; ทดสอบเงื่อนไข; การเปลี่ยนค่าในแต่ละรอบ) {งานที่กำหนดให้กระทำ}

เช่น
for (เตรียมหัวหอมและเตรียมมีด; หั่นยังไม่เสร็จ; ยกมีดและขยับมีด) หั่น;

แปลว่า
เตรียมหัวหอมและเตรียมมีด -> หั่น -> ยกมีดและขยับมีด -> ตรวจสอบว่า หั่นเสร็จแล้วยัง ถ้าไม่เสร็จ ให้กลับไปหั่นใหม่ซะ

ยกตัวอย่าง
for (0 => int i; i < 10; i + 1 => i) <<< i >>>;
จะแปลว่าอะไรล่ะ?

อ้อ ก็แปลว่า
เตรียมค่า i โดยเริ่มต้นจาก 0
พิมพ์ค่า i ออกมา
เอา i มาบวกด้วย 1 แล้วเก็บกลับเข้าไปที่ i
i ยังน้อยกว่า 10 อีกหรือเปล่า? ถ้าน้อยกว่าให้กลับไป พิมพ์ ใหม่

เอ้า ลองรันดู ได้ผลลัพท์ออกมาดังนี้:
0 :(int)
1 :(int)
2 :(int)
3 :(int)
4 :(int)
5 :(int)
6 :(int)
7 :(int)
8 :(int)
9 :(int)

อืมๆ นี่มันทำครบสิบรอบพอดีเลย!
แล้วอาจจะงงว่า แล้วทำไมมันไม่พิมพ์เลข 10 ด้วย?

อ๋อ ก็เพราะว่า ตอนที่มันเท่ากับ 10 แล้ว
พอทำการทดสอบ อ่าว มึงไม่น้อยกว่า 10 ซะแล้ว
ก็เลย หยุดการทำงานแต่เพียงเท่านั้น

วันนี้ก็พอรู้จักเรื่องการทำ ลูป ซ้ำๆ ไปหลายอย่างครับ
น่าจะเอาไปเขียนอะไรเล่นได้หลายอย่างกันเลย
จะมาเพิ่มเติมให้อีก เกี่ยวกับสัญลักษณ์ต่างๆ
เพื่อใครฟิต อยากลุกขึ้นมาเอามาใช้ประโยชน์

ทบทวน:
do { งานที่ให้ทำ }
while ( ทดสอบ .. ถ้าเป็นจริง ให้กลับไปทำงานนั้นต่อไป );

while ( ทดสอบ .. ถ้าเป็นจริง ให้ทำงานนี้ ) { ก็ไอ้งานที่จะให้ทำนี่แหละ }

for ( เตรียมตัว; ทดสอบว่าเป็นจริงไหม ถ้ายังเป็นจริง ให้ทำเรื่อยๆ; ทำเสร็จแล้วให้เปลี่ยนแปลงค่าอย่างไร ) { งานที่จะให้ทำ }

ไอ้ for นี่อ่านแล้วมันจะงงนิดๆ แต่พอใช้ไปใช้มา มันจะเข้าใจง่าย

คราวนี้ เรามาทบทวนอย่างอื่นบ้าง
ข้อมูลชนิด dur (ย่อมาจาก duration) คือ ระยะเวลา..
เช่น ms (มิลิวินาที), second, minute, hour, day, week พวกนี้เป็น dur ทั้งนั้น
หรือมีกำหนดละเอียดขึ้นเช่น 1.2::second แปลว่า 1.2 วินาที
พวกนี้เป็น dur ทั้งนั้น

Std.rand2(จำนวนเต็ม, จำนวนเต็ม) เป็นฟังก์ชั่นที่ให้ผลออกมาเป็น ค่าสุ่ม จำนวนเต็ม ระหว่างค่าที่กำหนดทั้งสอง

นอกนั้น มีสัญลักษณ์เกี่ยวกับการทดสอบค่าต่างๆอีกเช่น

== แปลว่า "เท่ากันหรือเปล่า"
< แปลว่า "น้อยกว่าหรือเปล่า"
> แปลว่า "มากกว่าหรือเปล่า"
<= แปลว่า "น้อยกว่า หรือ เท่ากับ หรือเปล่า"
>= แปลว่า "มากกว่า หรือ เท่ากับ หรือเปล่า"
!= แปลว่า "ไม่เท่ากับหรือเปล่า"

สำหรับข้อมูลชนิด int นี่
เรายังมีวิธีการในการ บวก หรือ ลบ มันทีละหนึ่งง่ายๆ
โดยการใช้สัญลักษณ์ ++ และ --

เช่น

i + 1 => i;

เขียนเป็น i++; ได้เลย ให้ผลกับ i อย่างเดียวกัน
คือเป็นการเพิ่ม i ทีละ 1

ผมไปพักผ่อนก่อนละนะ ทุกคน อย่าเพิ่งหลับดิ่วะ -_-"
เขียนยาวหน่อยทำหลับ
บอกแล้วให้ลองเล่นชัก มันสนุก -_-
อ่านอย่างเดียวง่วงตายชัก


กิจจาศักดิ์ ตริยานนท์
View full profile