07 August 2013

การทำ Text To Speech เพื่ออ่านข้อความที่ต้องการ

Updated on


        การทำให้แอนดรอยด์อ่านข้อความที่กำหนดให้ออกมาเป็นเสียง ต้องบอกเลยว่าทุกวันนี้ทำได้ไม่ยากแล้ว และสามารถรองรับกับการอ่านข้อความภาษาไทยได้แล้ว ซึ่งต่างจากเมื่อก่อนที่ตอนนั้นยังไม่รองรับภาษาไทย 

        Text To Speech (ขอเรียกสั้นๆว่า TTS) เป็นการส่งข้อความให้ระบบแปลงข้อความออกมาให้กลายเป็นเสียง ซึ่งตอนนี้ถือว่าเป็นฟีเจอร์พื้นฐานอย่างหนึ่งไปแล้วล่ะ ไม่ว่าเครื่องไหนก็ทำได้

        โดยที่อุปกรณ์แอนดรอยด์แต่ละยี่ห้อก็อาจจะมี TTS Engine แตกต่างกันออกไป แต่จะมี TTS ที่ทาง Google ทำใส่ไว้ในแอนดรอยด์เป็นตัวหลัก อาจจะมี TTS Engine อื่นๆเพิ่มเข้ามาตามแต่ละยี่ห้อ อย่างเช่น Samsung เค้าก็จะมี TTS Engine ของตัวเองใส่เข้ามาด้วย ก็สามารถเลือกใช้งานได้ตามต้องการ

สิ่งที่ควรรู้เมื่อเรียกใช้งาน Text To Speech

Engine ที่เรียกใช้งานได้

        อย่างที่บอกในตอนแรกครับว่าการทำงานของโปรแกรมสำหรับ TTS นั้นจะเรียกว่า Engine และในอุปกรณ์แอนดรอยด์แต่ละเครื่องเนี่ย สามารถมี Engine ได้หลายตัวครับ จะติดตั้งเพิ่มจาก Google Play Store ก็ยังได้ ดังนั้นก่อนจะเรียกใช้งานก็ควรรู้ว่าในเครื่องนั้นๆมี Engine ตัวไหนให้ใช้งานอยู่ครับ ซึ่งโดยปกติแล้วแต่ละยี่ห้อจะมีการกำหนด Default Engine ไว้แตกต่างกัน อย่างเช่นของ Samsung ก็จะกำหนด TTS Engine ของ Samsung ให้เป็น Default ไว้

ภาษาที่รองรับใน Engine นั้นๆ

        เพราะ TTS Engine ไม่ได้รองรับทุกภาษาเสมอไปครับ ดังนั้นควรตรวจสอบให้ดีๆก่อนว่า TTS Engine ที่จะเรียกใช้งานนั้นรองรับภาษาที่ต้องการใช้มั้ย? 

        โดยปกติแล้วจะรองรับภาษาพื้นฐานอยู่ 7 ภาษาด้วยกันครับ
                • เยอรมัน (de_DE)
                • อังกฤษ (en_GB)
                • อังกฤษ (en_US)
                • สเปน (es_ES)
                • ฝรั่งเศส (fr_FR)
                • อิตาลี (it_IT)
                • รัสเซีย (ru_RU)

        เมื่อนานมาแล้วสมัยที่เจ้าของบล็อกทำบทความนี้ครั้งแรก ได้ลองทดสอบดูแล้วพบว่าเครื่อง Samsung Galaxy Nexus (Android 4.3) ที่เจ้าของบล็อกใช้ในตอนนั้นนั้นยังรองรับแค่ 7 ภาษาหลักๆอยู่ ซึ่งตอนนั้นก็ยังเป็น TTS Engine ของ Google สมัยนู้นนนนเลย แต่ในปัจจุบันนี้ก็สามารถอัพเดทภาษาเพิ่มเติมได้แล้ว ทำให้ล่าสุดรองรับภาษาไทยเป็นที่เรียบร้อยแล้ว เฮ


         สำหรับ TTS Engine อย่างของ Samsung ยังไม่มีภาษาไทยนะ (ทดสอบบน Samsung Galaxy Note 5 Android 5.1.1) ดังนั้นอย่าลืมคำนึงถึงเรื่องนี้ด้วย 


มาเริ่มเรียกใช้งานกัน!!

        บนแอนดรอยด์จะมีคลาสที่ชื่อว่า TextToSpeech ให้ใช้งานอยู่แล้ว โดยการเรียกใช้งาน TTS เนี่ย มันจะต้องทำการ Initialize ตัวเองก่อนครับ (New Object เมื่อไรมันก็เริ่ม Initialize ตอนนั้นน่ะแหละ) เมื่อ Initialize เสร็จมันจะส่ง Event มาบอกว่าเสร็จแล้ว

        ซึ่งรูปโค๊ดเริ่มต้นก็จะประมาณนี้

import android.app.Activity;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;

public class MainActivity extends Activity implements TextToSpeech.OnInitListener {
    private TextToSpeech tts;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tts = new TextToSpeech(this, this);
    }

    @Override
    public void onInit(int status) {
        // Do something here
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        tts.shutdown();
    }

}

        ในตัวอย่างเจ้าของบล็อกจะ Initialize ตั้งแต่ใน onCreate เลย (แล้วแต่) แล้วพอ onDestroy ก็ค่อยหยุดเรียกใช้งาน TTS (คำสั่งนี้จะ Clear Resource ของ TTS ทิ้งครับ)

        กรณีที่อยากเปลี่ยนไปใช้ Engine ตัวที่ต้องการ (อย่าลืมนะว่าต้องมีอยู่ในเครื่องด้วย) ก็สามารถกำหนดได้ตั้งแต่ตอนแรกเลย โดยระบุ Package Name ของ Engine ที่ต้องการเรียกใช้งาน

        ยกตัวอย่างเช่น เครื่องของเจ้าของบล็อกมี TTS Engine อยู่ 2 ตัวคือ com.google.android.tts กับ com.samsung.SMT ถ้าอยากใช้ของ Google ก็จะต้องกำหนดตั้งแต่ตอนแรกแบบนี้

tts = new TextToSpeech(this, this, "com.google.android.tts");

        เมื่อที่ onInit ถูกเรียก สิ่งแรกที่ควรทำก่อนคือเช็คว่า Initialize สำเร็จหรือไม่ เพราะบางทีอาจจะมีปัญหาบางอย่างก็ได้ (เช่นกำหนด Package Name ของ Engine ผิด หรือไม่มีอยู่ในเครื่อง)

        โดยผลลัพธ์ที่เป็นไปได้จะมีอยู่แค่ 2 กรณีคือ
                • TextToSpeech.SUCCESS
                • TextToSpeech.ERROR 

@Override
public void onInit(int status) {
    if(status == TextToSpeech.SUCCESS) {
        // Do something here
    }
}

        ถ้าพร้อมใช้งานแล้ว คำสั่งข้างในนั้นเหมาะสำหรับกำหนดค่าการทำงานของ TTS ครับ เช่นภาษา, เสียง หรือความเร็วของเสียง แต่เอาเข้าจริงกำหนดตอนไหนก็ได้แหละไม่ได้กำหนดตายตัวหรอกว่าต้องมากำหนดในนี้ (เผื่อบางแอพอยากจะเปลี่ยนภาษาของ TTS กลางคัน)

        อยากจะกำหนดภาษาที่ต้องการก็ใช้คำสั่ง

tts.setLanguage(Locale.ITALY);

        แต่ถ้าเป็นภาษาไทยจะไม่มี Locale.THAI ให้เลือก ดังนั้นต้องกำหนดแบบนี้แทน

tts.setLanguage(new Locale("th");

        และเวลาต้องการให้ TTS ทำงานก็แค่เรียกใช้คำสั่งง่ายๆแบบนี้

tts.speak(String message, int queueMode, HashMap<String, String> params);

        สำหรับ queueMode เป็นการกำหนดวิธีการทำงานของ TTS เมื่อมีสั่งงานซ้อนกัน (เช่นกด 2 ครั้งติด) ซึ่งในส่วนนี้สามารถกำหนดได้ 2 แบบ คือ
                • TextToSpeech.QUEUE_FLUSH : ถ้ามีคำสั่ง Speak ทำงานอยู่ก่อนหน้า ก็จะหยุดทันทีแล้ว Speak ตัวใหม่จะทำงานทันที
                • TextToSpeech.QUEUE_ADD : ถ้ามีคำสั่ง Speak ทำงานอยู่ก่อนหน้า ก็จะรอจนกว่าทำงานเสร็จ แล้วจึงทำงานต่อ

        ส่วน params มีไว้สำหรับส่งค่าบางอย่างให้กับ TTS ได้ครับ ซึ่งตรงนี้ให้ใช้เป็น Null ไป เพราะไม่ค่อยจำเป็นมากนักสำหรับการใช้งานทั่วไป

        เวลาใช้งานจริงก็จะเป็นแบบนี้

tts.speak("ข้อความที่ต้องการ", TextToSpeech.QUEUE_FLUSH, null);

        แต่สำหรับ API 21 ขึ้นไป (Android 5.0) ได้ประกาศ Deprecated คำสั่งนี้ไป ให้เปลี่ยนไปใช้คำสั่งแบบนี้แทน

tts.speak(CharSequence message, int queueMode, Bundle params, String utteranceId);

        จะเห็นว่า params ถูกเปลี่ยนจาก HashMap<String, String> ถูกเปลี่ยนไปใช้เป็น Bundle แทนครับ เพื่อความเหมาะสม แต่ก็อย่างที่บอกในตอนแรก ไม่ได้ใช้อยู่ดี ก็ให้กำหนดเป็น Null เหมือนเดิม

        แต่สำหรับ utteranceId ถูกเพิ่มเข้ามาใหม่ เอาไว้กำหนด ID ของข้อความที่จะให้ TTS ทำงานเฉยๆครับ ตั้งชื่อเป็น String ว่าอะไรก็ได้ หน้าที่ของมันคือใช้กับ Listener ที่คอยบอก Result ของ TTS ว่ามันทำงานสำเร็จหรือป่าว หรือว่าหยุดกลางคัน หรือว่ามีปัญหา ซึ่งถ้ามีข้อความเยอะมากที่ให้ TTS ทำงาน เราก็จะเช็คว่าเป็นข้อความชุดไหนจาก utteranceId นั่นแหละครับ แต่ว่าต้องใช้ร่วมกับคำสั่ง setOnUtteranceCompletedListener หรือ setOnUtteranceProgressListener ซึ่งเจ้าของบล้อกขี้เกียจอธิบายต่อ ก็ขอข้อส่วนนี้ไปเนอะ

        ดังนั้นถ้าอยากรองรับคำสั่งนั้นแบบเก่าและแบบใหม่จะใช้คำสั่งแบบนี้ก็ได้

private void speak(CharSequence message) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        tts.speak(message, TextToSpeech.QUEUE_FLUSH, null, "");
    } else {
        tts.speak(message.toString(), TextToSpeech.QUEUE_FLUSH, null);
    }
}

...

speak("ข้อความที่ต้องการ");

ใช้ไฟล์เสียงสำหรับคำที่ต้องการก็ได้นะ

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

        ตัว TTS ในแอนดรอยด์รองรับการเพิ่ม Earcon เข้าไปได้ตามต้องการ โดยจะเก็บไฟล์เสียงไว้ใน res > raw ก็ได้ หรือจะเก็บไว้ในเครื่องแล้วระบุเป็น Path ก็ทำได้เช่นกัน

        ยกตัวอย่างว่าเจ้าของบล็อกมีไฟล์เสียงแมวอยู่ใน res > raw > meow.mp3 และอยากจะให้ TTS อ่านคำว่า เมี๊ยว เป็นเสียงที่เตรียมไว้ ก็เพิ่ม Earcon ไว้ตอนที่ Initialize เสร็จแล้ว

tts.addEarcon(String earcon, String packageName, int resourceId);

        เวลาใช้งานจริงก็จะประมาณนี้

...

public class MainActivity extends Activity implements TextToSpeech.OnInitListener {
    ...

    @Override
    public void onInit(int status) {
        tts.addEarcon("เมี๊ยว", getPackageName(), R.raw.whistle);
    }

    ...
}

        ส่วนเวลาสั่งงานไม่ได้ใช้คำสั่งเดิมนะ แต่จะใช้คำสั่งนี้แทน

playEarcon("เมี๊ยว");

...

private void playEarcon(String earcon) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        tts.playEarcon(earcon, TextToSpeech.QUEUE_FLUSH, null, "");
    } else {
        tts.playEarcon(earcon, TextToSpeech.QUEUE_FLUSH, null);
    }
}
        ซึ่ง Earcon จะต่างจากปกติตรงที่ ไม่สามารถกำหนดเป็นคำหนึ่งคำที่อยู่ในประโยคได้ ถ้าจะกำหนดก็ต้องกำหนดทั้งประโยคเลย หรือตัดให้เหลือแค่คำนั้นแค่คำเดียว เพราะมันไม่ใช่ TTS จริงๆ มันเป็นแค่การกำหนดคำหนึ่งคำให้เล่นไฟล์เสียงที่ต้องการ ดังนั้นในตัวอย่างเจ้าของบล็อกจึงเรียกคำสั่งด้วยคำว่า "เมี๊ยว" เท่านั้น ถ้าไปพิเรนทร์ใส่เป็น "แมว เหมียว ร้อง เมี๊ยว" ก็จะไม่มีผลอะไร

เพิ่มเติม

        รู้สึกว่ามันวุ่นวายจังเนอะ ต้องกำหนดอะไรก็ไม่รู้ตั้งเยอะแยะ ต้องมา Initialize ต้องรอมันพร้อมถึงจะใช้งานได้ และเมื่อไม่ใช้ต้องมานั่งเคลียร์ทิ้ง

        ดังนั้นเจ้าของบล็อกจึงขอแนะนำวิธีง่ายๆครับ ก็คือทำเป็น Class เอาไว้เรียกใช้งานแยกเลย ซึ่งเจ้าของบล็อกก็เขียนมาให้แล้วแหละ เผื่ออยากจะเอาไปใช้ให้มันง่ายขึ้น

MyTTS.java (จะเปลี่ยนเป็นชื่ออะไรก็ได้ตามใจชอบ)
import android.content.Context;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;

import java.util.Locale;

public class MyTTS extends UtteranceProgressListener implements TextToSpeech.OnInitListener, TextToSpeech.OnUtteranceCompletedListener {
    public static MyTTS myTTS;

    public static MyTTS getInstance(Context context) {
        if (myTTS == null) {
            myTTS = new MyTTS(context);
        }
        return myTTS;
    }

    private Context context;
    private TextToSpeech tts;
    private Locale locale = Locale.getDefault();
    private String enginePackageName;
    private String message;
    private boolean isRunning;
    private int speakCount;

    public MyTTS(Context context) {
        this.context = context;
    }

    public void speak(String message) {
        this.message = message;

        if (tts == null || !isRunning) {
            speakCount = 0;

            if (enginePackageName != null && !enginePackageName.isEmpty()) {
                tts = new TextToSpeech(context, this, enginePackageName);
            } else {
                tts = new TextToSpeech(context, this);
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                tts.setOnUtteranceProgressListener(this);
            } else {
                tts.setOnUtteranceCompletedListener(this);
            }

            isRunning = true;
        } else {
            startSpeak();
        }
    }

    public MyTTS setEngine(String packageName) {
        enginePackageName = packageName;
        return this;
    }

    public MyTTS setLocale(Locale locale) {
        this.locale = locale;
        return this;
    }

    private void startSpeak() {
        speakCount++;

        if (locale != null) {
            tts.setLanguage(locale);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            tts.speak(message, TextToSpeech.QUEUE_FLUSH, null, "");
        } else {
            tts.speak(message, TextToSpeech.QUEUE_FLUSH, null);
        }
    }

    private void clear() {
        speakCount--;

        if (speakCount == 0) {
            tts.shutdown();
            isRunning = false;
        }
    }

    @Override
    public void onInit(int status) {
        if (status == TextToSpeech.SUCCESS) {
            startSpeak();
        }
    }

    @Override
    public void onStart(String utteranceId) {
    }

    @Override
    public void onDone(String utteranceId) {
        clear();
    }

    @Override
    public void onError(String utteranceId) {
        clear();
    }

    @Override
    public void onUtteranceCompleted(String utteranceId) {
        clear();
    }
}


        เวลาเรียกใช้งานก็จะเหลือโค้ดสั้นๆแค่นี้

MyTTS.getInstance(Context context).speak(String message);

         ตอนใช้งานจริงก็กำหนดแค่นิดเดียวเอง

import android.app.Activity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends Activity implements View.OnClickListener {
    private Button btnSay;

    ...

    @Override
    public void onClick(View v) {
        if(v == btnSay) {
            MyTTS.getInstance(this)
                    .speak("Hello Android. It's me, Google!");
        }
    }
}

        ง่ายขึ้นมั้ยล่ะ!!

        ถ้าอยากจะระบุภาษาที่ต้องการด้วยก็แค่ใช้คำสั่งแบบนี้

MyTTS.getInstance(this)
        .setLocale(Locale.GERMANY)
        .speak("Hallo Welt");

        กำหนด Engine ที่ต้องการก็ได้นะ

MyTTS.getInstance(this)
        .setEngine("com.google.android.tts")​
        .setLocale(new Locale("th"))
        .speak("จะสะดวกสบายกันเกินไปแล้วนะ!!");

        น่าจะช่วยให้ผู้ที่หลงเข้ามาอ่านสามารถทำ TTS กันได้ง่ายขึ้นเนอะ