【Excel】利用 Windings2 字型製作 打勾/打叉 效果

在儲存格輸入 P(注意,要大寫) → 選擇字型 Windings2(這是一種特殊符號字型),就變成打勾了

下圖為 大寫英文Windings2 字型顯示的效果


有興趣的朋友也可以玩玩其他文字(例如小寫英文)

【Android】Nougat(Android7, API24) 在 APP 加入自己信任的憑證

參考資料 ----

7.1.1版以前的Android 裝置2021年9月起將不支援Let's Encrypt憑證

為什麼網站使用Let’s Encrypt SSL憑證卻無法正常訪問?

der pem cer crt key pfx等概念及区别

Let's Encrypt Certificates

网络安全配置



如果在 APP 內有(包括但不限於) 使用到 webview 元件 或 urlconnectionvolley,並且要 訪問/瀏覽/request 的網站是使用 Let's Encrypt 的憑證,則在 Android7.1.1(Nougat, API24~25)(含) 以前的裝置會出現下述的訊息,而無法 訪問/瀏覽/request 網站。

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

這個現象,即使在 AndroidStudio模擬器也會發生,所以如果您的 APP 的 minSdkVersion 是在 24(含) 以下,建議您建個 API24 的模擬器檢查一下您的 APP,看是否運作正常。


對應的方法,就是在 APP 加入自己信任的憑證。


打開 Chrome 造訪您自己的網站, 點 網址旁的 鎖



→ 已建立安全連線



→ 憑證有效



→ 可以看到 "簽發者" 是 "R3"




Let's Encrypt 的 簽發證書列表 https://letsencrypt.org/certificates/

找到

Let’s Encrypt R3 (RSA 2048, O = Let's Encrypt, CN = R3)

Signed by ISRG Root X1: der, pem, txt

下載 lets-encrypt-r3.der 到 /專案/res/raw/ 下, 修改檔名, 去除連字號變成 letsencryptr3.der


建立 /專案/res/xml/security.xml

 
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">(不含www.)您的域名</domain>
        <trust-anchors>
            <certificates src="@raw/letsencryptr3"/> der 證書檔名
        </trust-anchors>
    </domain-config>
</network-security-config>
 



修改 AndroidManifast.xml

 
<application
    ...
    ...
    android:networkSecurityConfig="@xml/security"    因為這個屬性是對 API24 以上, 我的 app minSdkVersion=23
    tools:targetApi="n"                     所以 AndroidStudio 會再補上這個屬性
    ...
    ... >
 


就 ok 了

不過對 API23(含) 以下的 Android 裝置無效,再找時間研究了。





2021.11.21
意外的發現,因為暫時解決不了 API23 的問題,所以老人家就想說...先將 minSdkVersion=24 擋一下,等問題解決了再改回原 minSdkVersion
結果上架更新後,老人家的 華為 T3(API23) 平板仍然可以在 Google Play Store 搜的到 APP,只不過在 APP 介紹頁面會顯示 "這個應用程式可能無法針對你的裝置進行最佳化調整" 的警示,但還是可以更新,而且更新後,竟然就恢復正常了!! 送出 request 可以正常得到回應,也可以正常下載檔案了!!

【Android】Layout 加上背景圖

先將圖片放置於 /res/drawable 目錄下,本例放了一張直的 teddy.jpg 和一張 不足螢幕尺寸的小圖片 tile.jpg

 

類型一

 
<???Layout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/teddy" >
 
結果如下


類型二
當圖片不足螢幕尺寸時,layout 會將照片放大填滿整個螢幕,圖片會失真、糊掉。
 
<???Layout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/tile" >
 



但我們想要呈現的效果是像磁磚一樣,將同一張小圖片多張、重複性地填滿螢幕

先建立一個重複圖片的 main_tile.xml
 
<?xml version="1.0" encoding="utf-8"?>
<bitmap 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/tile"
    android:tileMode="repeat" />
 


然後在 layoutbackground 屬性填入 main_tile.xml

 
<???Layout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/main_tile" >



就不會像前一張那樣,圖片的顆粒被放大,呈現出模糊的效果。

【Oracle】資料表填入另一資料表的欄位值

假設有 2 個資料表 aa, bb

aafield1, field2 值要填入 bbfield3, field4


 
UPDATE (SELECT aa.field1, aa.field2, bb.field3, bb.field4 FROM aa, bb
            WHERE aa.dno=bb.dno -- 將 aa, bb 兩個資料表關聯
                AND SUBSTR(aa.dno,1,1)='D'  -- 判斷條件式
                AND buydate BETWEEN '20180101' AND '20181231'
                AND buyemp='12345678'
                AND aa.field1 is null)
   SET field1=field3, field2=field4
 


【Android】色表

自己收集多年,建立的色表,有需要的朋友請自行取用

 

 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>

    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
    <color name="holo_light_primary">#010101</color>

    <color name="colorRed">#FF0000</color>
    <color name="colorBlack">#000000</color>
    <color name="LightBlack">#3F3F3F</color>

    <color name="actionbar_title_color">#224894</color>
    <color name="listitem_background_color">#ffffff</color>
    <color name="description">#0099FF</color>
    <color name="AliceBlue">#F0F8FF</color>
    <color name="AntiqueWhite">#FAEBD7</color>
    <color name="Aqua">#00FFFF</color>
    <color name="Aquamarine">#7FFFD4</color>
    <color name="Azure">#F0FFFF</color>
    <color name="Beige">#F5F5DC</color>
    <color name="Bisque">#FFE4C4</color>
    <color name="BlanchedAlmond">#FFEBCD</color>
    <color name="blue">#0000FF</color>
    <color name="BlueViolet">#8A2BE2</color>
    <color name="Brown">#A52A2A</color>
    <color name="BurlyWood">#DEB887</color>
    <color name="CadetBlue">#5F9EA0</color>
    <color name="Chartreuse">#7FFF00</color>
    <color name="Chocolate">#D2691E</color>
    <color name="Coral">#FF7F50</color>
    <color name="CornflowerBlue">#6495ED</color>
    <color name="Cornsilk">#FFF8DC</color>
    <color name="Crimson">#DC143C</color>
    <color name="cyan">#ff00ffff</color>
    <color name="dkblue">#00008B</color>
    <color name="dkCyan">#008B8B</color>
    <color name="dkGoldenRod">#B8860B</color>
    <color name="dkgray">#ff444444</color>
    <color name="dkKhaki">#BDB76B</color>
    <color name="dkMagenta">#8B008B</color>
    <color name="dkOliveGreen">#556B2F</color>
    <color name="dkorange">#FF8C00</color>
    <color name="dkOrchid">#9932CC</color>
    <color name="dkRed">#8B0000</color>
    <color name="dkSalmon">#E9967A</color>
    <color name="dkSeaGreen">#8FBC8F</color>
    <color name="dkSlateBlue">#483D8B</color>
    <color name="dkSlateGray">#2F4F4F</color>
    <color name="dkTurquoise">#00CED1</color>
    <color name="dkViolet">#9400D3</color>
    <color name="deepPink">#FF1493</color>
    <color name="deepSkyBlue">#00BFFF</color>
    <color name="DimGray">#696969</color>
    <color name="DodgerBlue">#1E90FF</color>
    <color name="FireBrick">#B22222</color>
    <color name="FloralWhite">#FFFAF0 </color>
    <color name="ForestGreen">#228B22</color>
    <color name="Fuchsia">#FF00FF </color>
    <color name="Gainsboro">#DCDCDC</color>
    <color name="GhostWhite">#F8F8FF</color>
    <color name="Gold">#FFD700</color>
    <color name="GoldenRod">#DAA520</color>
    <color name="gray">#ff888888</color>
    <color name="green">#ff00ff00</color>
    <color name="GreenYellow">#ADFF2F</color>
    <color name="HoneyDew">#F0FFF0 </color>
    <color name="HotPink">#FF69B4</color>
    <color name="IndianRed">#CD5C5C</color>
    <color name="Indigo">#4B0082</color>
    <color name="Ivory">#FFFFF0 </color>
    <color name="Khaki">#F0E68C</color>
    <color name="Lavender">#E6E6FA</color>
    <color name="LavenderBlush">#FFF0F5</color>
    <color name="LawnGreen">#7CFC00</color>
    <color name="LemonChiffon">#FFFACD</color>
    <color name="ltBlue">#ADD8E6</color>
    <color name="ltCoral">#F08080</color>
    <color name="ltCyan">#E0FFFF</color>
    <color name="ltGoldenRodYellow">#FAFAD2</color>
    <color name="ltgray">#ffcccccc</color>
    <color name="ltGreen">#90EE90</color>
    <color name="ltPink">#FFB6C1</color>
    <color name="ltSalmon">#FFA07A</color>
    <color name="ltSeaGreen">#20B2AA</color>
    <color name="ltSkyBlue">#87CEFA</color>
    <color name="ltSlateGray">#778899</color>
    <color name="ltSlateGrey">#778899</color>
    <color name="ltSteelBlue">#B0C4DE</color>
    <color name="ltYellow">#FFFFE0</color>
    <color name="Lime">#00FF00</color>
    <color name="LimeGreen">#32CD32</color>
    <color name="Linen">#FAF0E6</color>
    <color name="magenta">#ffff00ff</color>
    <color name="Maroon">#800000</color>
    <color name="MediumAquaMarine">#66CDAA</color>
    <color name="MediumBlue">#0000CD</color>
    <color name="MediumOrchid">#BA55D3</color>
    <color name="MediumPurple">#9370D8</color>
    <color name="MediumSeaGreen">#3CB371</color>
    <color name="MediumSlateBlue">#7B68EE 	  	</color>
    <color name="MediumSpringGreen">#00FA9A</color>
    <color name="MediumTurquoise">#48D1CC</color>
    <color name="MediumVioletRed">#C71585</color>
    <color name="MidnightBlue">#191970</color>
    <color name="MintCream">#F5FFFA</color>
    <color name="MistyRose">#FFE4E1</color>
    <color name="Moccasin">#FFE4B5</color>
    <color name="NavajoWhite">#FFDEAD</color>
    <color name="Navy">#000080</color>
    <color name="OldLace">#FDF5E6</color>
    <color name="Olive">#808000</color>
    <color name="OliveDrab">#6B8E23</color>
    <color name="orange">#FFA500</color>
    <color name="OrangeRed">#FF4500</color>
    <color name="Orchid">#DA70D6</color>
    <color name="PaleGoldenRod">#EEE8AA</color>
    <color name="PaleGreen">#98FB98</color>
    <color name="PaleTurquoise">#AFEEEE</color>
    <color name="PaleVioletRed">#D87093</color>
    <color name="PapayaWhip">#FFEFD5</color>
    <color name="PeachPuff">#FFDAB9</color>
    <color name="Peru">#CD853F</color>
    <color name="pink">#FFC0CB</color>
    <color name="Plum">#DDA0DD</color>
    <color name="PowderBlue">#B0E0E6</color>
    <color name="Purple">#800080</color>
    <color name="red">#ffff0000</color>
    <color name="RosyBrown">#BC8F8F</color>
    <color name="RoyalBlue">#4169E1</color>
    <color name="SaddleBrown">#8B4513</color>
    <color name="Salmon">#FA8072</color>
    <color name="SandyBrown">#F4A460</color>
    <color name="SeaGreen">#2E8B57</color>
    <color name="SeaShell">#FFF5EE</color>
    <color name="Sienna">#A0522D</color>
    <color name="Silver">#C0C0C0</color>
    <color name="SkyBlue">#87CEEB</color>
    <color name="SlateBlue">#6A5ACD</color>
    <color name="SlateGray">#708090</color>
    <color name="SlateGrey">#708090</color>
    <color name="Snow">#FFFAFA</color>
    <color name="SpringGreen">#00FF7F</color>
    <color name="SteelBlue">#4682B4</color>
    <color name="Tan">#D2B48C</color>
    <color name="Teal">#008080</color>
    <color name="Thistle">#D8BFD8</color>
    <color name="Tomato">#FF6347</color>
    <color name="Turquoise">#40E0D0</color>
    <color name="Violet">#EE82EE</color>
    <color name="Wheat">#F5DEB3</color>
    <color name="white">#FFFFFF</color>
    <color name="WhiteSmoke">#F5F5F5</color>
    <color name="yellow">#FFFF00</color>
    <color name="YellowGreen">#9ACD32</color>

    <color name="transparent">#00000000</color>
</resources>
 

【Kotlin】微軟的 文字轉換語音(Text To Speach, TTS)

參考資料 ----

文字轉換語音

免費試用語音服務

開始使用文字轉換語音

適用於語音服務的 Azure 訂用帳戶金鑰

Github 範例

cognitive-services-speech-sdk

 

大家應該都聽過 Google 小姐說話,老人家好奇微軟是否也有類似的服務 -- 果然也有 MS 小姐,而且不只小姐,還有先生喔!而且,還不只一位喔。😄

 

用 手機 或 電腦 的瀏覽器,到上面 "參考資料" 的網址,往下捲動,會看到標題 "透過這個示範應用程式 (採用 JavaScript SDK 建置) 試用文字轉換語音功能",下方的 "文字"  欄可以清除裡面預設的文字,輸入您想要 MS 小姐朗讀的內容。

 

右方 "語言" 預設是 Chinese(Taiwanese Mandarin),還可以切換到 Chinese(Mandarin, Simplified) 簡体中文Chinese(Cantonese, Traditional) 廣東話

 

"語音" 則有 HsiaoChen(Neutral) - 曉臻HsiaoYu(Neutral) - 曉雨YunJhe(Neutral) - 雲哲HanHan - 涵涵Yating - 雅婷Zhiwei - 志威,有 (Neutral) 的表示是以 AI 合成的,標榜聲音更自然,也因此收費較高(2021.08.12 定價表);不過不用担心,不論選擇哪一種語音,每個月有免費額度,超過才需要付費。

 

"說話風格",Taiwanese 語言沒得選說話風格,只有 General,目前只支援简体中文有多種風格。

 

"速度" 應該不需解釋

"音調" 若調低就會像男生,但不像 Google 小姐會變鴨嗓


調整好您的喜好後,按 "播放" 鍵,會有一小段時間的寂靜,猜測應該是系統正在合成語音中,在播放中變更設定是沒有作用的,必須先按 "停止" 再變更。但之後寫成 APP,並不會有網頁的延遲現象。

中英文混著唸大致上也還可以,英文不會怪腔怪調的。

變更 "語言" 或 玩了一陣子後,如果覺得怪怪的,就按一下瀏覽器的 "重新整理"。

 

我去複製了維基的龜兔賽跑,發現 "睡覺" 简体中文會唸 ㄕㄨㄟˋ  ㄐㄩㄝˊ,問了大陸的朋友後,確認這是简体中文的 bug

後來又想,會不會是正體字的關係?於是,我將 "睡覺" 轉成 簡體字 "睡觉",果然發音就正確了 ;我再讓繁體中文的語音去唸簡體字的"睡觉" ,不意外地,也是唸 ㄕㄨㄟˋ  ㄐㄩㄝˊ -- 原來 AI 並沒有把繁簡差異納入考量,以此類推,雖都是英語系國家,同個字可能有不一樣的發音,所以在為該國家(地區) 挑選語音角色時,還是儘量以微軟為該國(地區) 設計的角色為優先選擇。

 



因為要收費,所以您必須具有微軟 Azure 雲端服務帳戶,沒有的話,就去申請一個 -- 參考 免費試用語音服務


登入 Azure 首頁,按一下左上角的 "功能表"

 

點擊 "建立資源"



在 搜尋欄 輸入 speech



輸入相應的資料,有點詭異的是...老人家是第一次使用,但輸入 "名稱" 時卻一直回應說已存在,換了好幾個名字才通過。

"資源群組" 因為是第一次建,所以要按一下 "新建"



出現 "您的部署已完成",按一下 "前往資源"



點擊 "按一下這裡以管理金鑰"



按一下 "顯示金鑰",金鑰會以明碼顯示


有兩組金鑰,只要選 1 組即可

兩組的用意是:

當您想變更金鑰 1 時,新的金鑰並不會立即生效(可能要過 24 小時後),而您的 app、網站、... 提供的服務又不能中斷,這時就可以先改用金鑰 2



系統需求:

Android Studio 3.3 以上

Android 6.0(Marshmallow, API23) 以上


建立新專案

 

修改專案層級的 build.gradle

 

...
...

allprojects {
    repositories {
        google()
        mavenCentral()
        
        // 加這個
        maven {
            url 'https://csspeechstorage.blob.core.windows.net/maven/'
        }
    }
}
 


 


修改 app 層級的 build.gradle

 
...
...

android {
    
    defaultConfig {
        ...
        ...
        
        minSdkVersion 23    // 注意要 23 以上
        
        ...
        ...
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    ...
    ...
}

dependencies {

    ...
    ...
    
    // Speech SDK
    implementation 'com.microsoft.cognitiveservices.speech:client-sdk:1.18.0'
}
 


AndroidManifest.xml
 
...
...

<uses-permission android:name="android.permission.INTERNET" />

<application
    ...
    ...

 


strings.xml
 
...

<string name="tw">有一位國王,他擁有一座美麗的花園,花園裡有一棵結著金蘋果的樹。國王派人每天清點樹上的蘋果有幾顆。</string>

...
 


activity_main.xml(這是簡化的 layout,重點只在表達放了這些元件)
 
...

<Button
    android:id="@+id/btnSpeak"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onbtnSpeakClicked"
    android:text="speak" />

<Button
    android:id="@+id/btnStop"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onbtnStopClicked"
    android:text="stop" />

<EditText
    android:id="@+id/edtSpeech"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/tw" />

<TextView
    android:id="@+id/outputMessage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="outputMessage" />

...
 


MainActivity.kt
 
...

class MainActivity : AppCompatActivity() 
{
    private val TAG = "MainActivity"
    private var speechConfig: SpeechConfig? = null
    private var synthesizer: SpeechSynthesizer? = null
    private lateinit var connection: Connection
    private var audioTrack: AudioTrack? = null
    lateinit var outputMessage: TextView
    private var speakingRunnable: SpeakingRunnable? = null
    private var singleThreadExecutor: ExecutorService? = null
    private val synchronizedObj = Any()
    private var stopped = false
    
    
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 請求取得網路授權
        val requestCode = 5 // Unique code for the permission request
        ActivityCompat.requestPermissions(this,
                                            arrayOf(Manifest.permission.INTERNET),
                                            requestCode
                                        )

        singleThreadExecutor = Executors.newSingleThreadExecutor()
        speakingRunnable = SpeakingRunnable()
        outputMessage = findViewById(R.id.outputMessage)
        outputMessage.setMovementMethod(ScrollingMovementMethod())
        audioTrack = AudioTrack(AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build(),
            AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(24000) // 音調
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                .build(),
            AudioTrack.getMinBufferSize(24000,
                AudioFormat.CHANNEL_OUT_MONO,
                AudioFormat.ENCODING_PCM_16BIT
            ) * 2,
            AudioTrack.MODE_STREAM,
            AudioManager.AUDIO_SESSION_ID_GENERATE
        )
        // 設定說話速度, 正常速度是 1
        val params = PlaybackParams()
        params.speed = 0.7.toFloat()
        audioTrack!!.playbackParams = params

        // 設定音量, float: 0 ~ 1.0
        // audioTrack.setVolume((float) 0.01);
    }
    
    
    override fun onResume()
    {
        super.onResume()
        
        // 初始化
        CreateSynthesizer()
        
        // 連線 Azure 主機
        ConnectAzure()
    }
        
    override fun onDestroy()
    {
        super.onDestroy()

        if (synthesizer != null)
        {
            synthesizer.close()
            connection.close()
        }
        if (speechConfig != null)
        {
            speechConfig.close()
        }
        if (audioTrack != null)
        {
            singleThreadExecutor!!.shutdownNow()
            audioTrack!!.flush()
            audioTrack!!.stop()
            audioTrack!!.release()
        }
    }
    
    
    // 初始化
    fun CreateSynthesizer()
    {
        if (synthesizer != null)
        {
            speechConfig!!.close()
            synthesizer!!.close()
            connection.close()
        }

        Log.d(TAG, "初始化...")
        speechConfig = SpeechConfig.fromSubscription(speechSubscriptionKey, serviceRegion)
        // 使用較好的 24kHz 音質
        speechConfig!!.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm)

        // 設定語音角色
        // 語音列表: https://aka.ms/csspeech/voicenames
        // AI, 合成的聲音更接近真人, 收費較高
        // speechConfig.setSpeechSynthesisVoiceName("en-US-GuyNeural");
        // speechConfig.setSpeechSynthesisVoiceName("en-US-JennyNeural");
        // speechConfig.setSpeechSynthesisVoiceName("en-US-AriaNeural");
        // speechConfig.setSpeechSynthesisVoiceName("zh-TW-HsiaoChenNeural");  // 曉臻

        // 標準
        // speechConfig.setSpeechSynthesisVoiceName("en-US-ZiraRUS");   // 美, Zira
        // speechConfig.setSpeechSynthesisVoiceName("en-GB-HazelRUS");   // 英, Hazel
        // speechConfig.setSpeechSynthesisVoiceName("ja-JP-HarukaRUS");   // 曰, 春香, Haruka
        speechConfig!!.setSpeechSynthesisVoiceName("zh-TW-Yating") // 台灣, 雅婷, 也適用於大陸
        // speechConfig.setSpeechSynthesisVoiceName("zh-HK-Danny");   // 廣東話, Danny
        synthesizer = SpeechSynthesizer(speechConfig, null)
        connection = Connection.fromSpeechSynthesizer(synthesizer)
        val current = resources.configuration.locale
        connection.connected.addEventListener { o: Any?, e: ConnectionEventArgs? -> updateOutputMessage("Connection established.\n")    }
        connection.disconnected.addEventListener { o: Any?, e: ConnectionEventArgs? -> updateOutputMessage("Disconnected.\n")   }
        synthesizer!!.SynthesisStarted.addEventListener { o: Any?, e: SpeechSynthesisEventArgs -> updateOutputMessage(String.format(current,"Synthesis started. Result Id: %s.\n", e.result.resultId))
                                                          e.close()
                                                        }
        synthesizer!!.Synthesizing.addEventListener { o: Any?,
                                                    e: SpeechSynthesisEventArgs -> updateOutputMessage(String.format(current, "Synthesizing. received %d bytes.\n", e.result.audioLength))
                                                      e.close()
                                                    }
        synthesizer!!.SynthesisCompleted.addEventListener { o: Any?, e: SpeechSynthesisEventArgs ->
            updateOutputMessage("Synthesis finished.\n")
            updateOutputMessage("""	First byte latency: ${e.result.properties.getProperty(PropertyId.SpeechServiceResponse_SynthesisFirstByteLatencyMs)} ms.""" )
            updateOutputMessage("""	Finish latency: ${e.result.properties.getProperty(PropertyId.SpeechServiceResponse_SynthesisFinishLatencyMs)} ms.""" )
            e.close()
        }
        synthesizer!!.SynthesisCanceled.addEventListener { o: Any?, e: SpeechSynthesisEventArgs ->
            val cancellationDetails = SpeechSynthesisCancellationDetails.fromResult(e.result).toString()
            updateOutputMessage(""" Error synthesizing. Result ID: ${e.result.resultId}. Error detail: ${System.lineSeparator()}$cancellationDetails${System.lineSeparator()}Did you update the subscription info? """.trimIndent(), true, true)
            e.close()
        }
        synthesizer!!.WordBoundary.addEventListener { o: Any?, e: SpeechSynthesisWordBoundaryEventArgs ->
            updateOutputMessage(String.format(current, 
                                              "Word boundary. Text offset %d, length %d; audio offset %d ms.\n",
                                              e.textOffset,
                                              e.wordLength,
                                              e.audioOffset / 10000
                                            )
                               )
        }
    }
    
    
    // 連線 Azure 主機
    fun ConnectAzure()
    {
        if (connection == null)
        {
            updateOutputMessage("Please initialize the speech synthesizer first\n", true, true)
            return
        }
        connection.openConnection(true)
        updateOutputMessage("Opening connection.\n")
    }
    
    
    // 開始播放
    fun onbtnSpeakClicked(vv: View?)
    {
        clearOutputMessage()
        
        if (synthesizer == null)
        {
            Log.d(TAG, "speech synthesizer not initialized")
            return
        }
        val speakText = findViewById<EditText>(R.id.edtSpeech)
        speakingRunnable!!.setContent(speakText.text.toString())
        singleThreadExecutor!!.execute(speakingRunnable)
    }
    
    
    // 停止播放
    fun onbtnStopClicked(vv: View?)
    {
        if (synthesizer == null)
        {
            Log.d(TAG, "speech synthesizer not initialized")
            return
        }
        stopSynthesizing()
    }
    
    
    private fun stopSynthesizing()
    {
        if (synthesizer != null)
        {
            synthesizer!!.StopSpeakingAsync()
        }
        if (audioTrack != null)
        {
            synchronized(synchronizedObj) { stopped = true }
            audioTrack!!.pause()
            audioTrack!!.flush()
        }
    }
    
    
    internal inner class SpeakingRunnable : Runnable
    {
        private var content: String? = null
        fun setContent(content: String?)
        {
            this.content = content
        }

        override fun run()
        {
            try
            {
                audioTrack!!.play()
                synchronized(synchronizedObj)
                {
                    stopped = false
                }
                val result = synthesizer!!.StartSpeakingTextAsync(content).get()
                val audioDataStream = AudioDataStream.fromResult(result)

                // Set the chunk size to 50 ms. 24000 * 16 * 0.05 / 8 = 2400
                val buffer = ByteArray(2400)
                while (!stopped)
                {
                    val len = audioDataStream.readData(buffer)
                    if (len == 0L)
                    {
                        break
                    }
                    audioTrack!!.write(buffer, 0, len.toInt())
                }
                audioDataStream.close()
            }
            catch (ex: Exception)
            {
                Log.e("Speech Synthesis Demo", "unexpected " + ex.message)
                ex.printStackTrace()
                assert(false)
            }
        }
    }
    
    
    private fun updateOutputMessage(text: String)
    {
        updateOutputMessage(text, false, true)
    }

    
    @Synchronized
    private fun updateOutputMessage(text: String, error: Boolean, append: Boolean)
    {
        runOnUiThread 
        {
            if (append)
                {
                    outputMessage.append(text)
                }
            else
                {
                    outputMessage.text = text
                }
            if (error)
            {
                val spannableText = outputMessage.text as Spannable
                spannableText.setSpan(ForegroundColorSpan(Color.RED),
                                      spannableText.length - text.length,
                                      spannableText.length,
                                      0
                                     )
            }
        }
    }
    
    
    private fun clearOutputMessage()
    {
        updateOutputMessage("", false, false)
    }
    

    companion object
    {
        // 申請的金鑰
        private const val speechSubscriptionKey = "blablablablablablablablabla"
        // 申請的 Azure 主機所在區域
        private const val serviceRegion = "eastasia"
    }

}
 


* 微軟支援多種語音輸出格式,如 rawoggmp3...,您可參考 SpeechSynthesisOutputFormat,以 音訊、網路連線 品質做為評估要點,選擇您喜好的格式;不過本例因採用 AudioTrack,所以只能播 Raw...Pcm,要播其他格式,應該需要調整播放方式。

在程式碼播放格式為 Raw24Khz16BitMonoPcm,若改為 Raw16Khz16BitMonoPcm 則音調變高且速度變快


* 如果您有玩過黑膠唱片,就知道將唱機轉速變慢,聲音就會變低沈;而 AudioTrackAPI22(含) 之前就是類似黑膠的效果, 但 API23 將速度放慢,聲調並不會變低沈,可能這也是微軟要求 minSdkVersion 23 的理由吧。

 

 * 似乎按 STOP 鈕後再按 SPEAK 鈕,就是從頭開始,沒辦法從停止處再接下去播放。 

目前想到的解法是:

因為是唸一段文章,中間必定會有標點符號,那就將一整段文章做分割(split) ,存入 List<String> 內,再逐個段落元素取出朗讀;但也不能切太小段,因為似乎也有提出請求的數量的限制,而且同一段文章,分割 段落 的 長短,微軟小姐的語調是有些差異的,目前踹出來的可行方式是以句點做為切割點,分段落切。

 

* 語音長度有限制,請參考 Text-to-Speech Quotas and limits per Speech resource ,若是長篇文章,要想辦法切成 10 分鐘內的片段。



然後,老人家又好奇了,微軟有 AI 語音,那 Google 有嗎?還真的有!而且 Amazon 也有,不過後兩者都沒有微軟的豐富,另外老人家覺得微軟的語音夠用了,就懶的再試了😁,有興趣的看倌就自行摸索吧。


相關筆記 ----

【Kotlin】TextToSpeech -- 請 Google 小姐朗讀

 

 

【Oracle】簡易計算年齡的方法

參考資料 ----

EXTRACT (datetime)

TO_DATE

 
    -- 以 2018 年為例
    -- 先以 TO_DATE 將字串欄位轉成日期型態
    -- 再以 EXTRACE 取出 其 出生年度
    -- 然後與 2018 相減, 就可得出 年齡了
    SELECT 2018-EXTRACT(YEAR  FROM  TO_DATE(birthday,'yyyymmdd')) age  FROM  table1
 


【Linux】設定 Rocky Linux 為 GUI 圖形介面模式

下載最小安裝 DVD iso


選擇 Server最小安裝


安裝完成後,會是文字介面,以 root 登入



[root]# dnf group list

會列出可安裝的群組類型

[root]# dnf groupinstall "Server with GUI" -y

再設定以 GUI 模式開機

[root]# systemctl set-default graphical

重新開機,就 ok

[root]# reboot

【Kotlin】SQLite 以 BIG5 排序

參考資料 ----

BIG5 碼線上查詢系統


這是個山不轉路轉的作法 😝


繁體中文習慣以 BIG5 排序,但 SQLiteORDER BY 是依 UTF8 排序,既然如此,那我們就轉個彎,在資料表內新增一個欄位,存放 BIG5 內碼,當需要排序時,就 ORDER BY 這個 BIG5 內碼欄位。

 

例如:

我們有個資料表,存放考試科目,則建立 2 個欄位 exname(考試科目名稱)、exbig5,當新增一筆記錄時,就查出 exname 的中文 BIG5 內碼 並存入 exbig5;當需要排序時,就 ORDER BY exbig5


先產生中文字的 BIG5 內碼表(參考底下的相關筆記)


將內碼表置於 /專案目錄/asset/


MAP 讀入內碼表(這屬於耗時的工作,應交由 Thread、AsyncTask 或 Coroutine 執行)


新增儲存記錄時,查詢 exname 內(逐字)的 BIG5 內碼,所得到的字串存入 exbig5


之後的查詢,若需要排序 exname 時,就改排序 exbig5



相關筆記 ----

【C#】以程式列出 中文字 與 BIG5 內碼 的對應表



【MySQL】簡體中文的 編碼與排序關係

參考資料 ----

MySQL Chinese, Japanese, and Korean Character Sets

一图弄懂ASCII、GB2312、GBK、GB18030编码

 

探索了正體中文的排序,好奇...那簡體中文呢?

 

MySQL 對簡體中文在 UTF8 時的排序似乎看不出規律性

一层楼, 一枕奇, 七侠五义, 万花楼, 三侠五义, 三国志, 三国演义, 三字经, 世说新语, 东周列国志, 九章算术, 乾隆下江南, 二刻拍案惊奇...

中間突然冒出了筆劃很多的 "乾隆下江南"


因為大陸是漢語拼音,所以若以 gb18030 編碼排序,看似是以拼音字母順序排列

-- 按漢語拼音排序
SELECT * FROM 資料表 ORDER BY CONVERT(欄位名 USING gb18030) 

結果如下:

八段锦, 八美图, 白圭志, 白虎通, 百家姓, 北里志, 北梦琐言, ...

"" 開頭的書名被排到後面去了!整個排序跟上面完全不同!


如果仿照正體中文的排序法呢?

SELECT * FROM 資料表 ORDER BY CONVERT(SUBSTR(欄位名,1,1) USING gb18030), BINARY 欄位名

結果如下:

八段锦, 八美图, 白圭志, 白虎通, 百家姓, 北梦琐言, 北里志, ...

沒有很大差異,僅 北梦琐言, 北里志 前後對調

 

相關筆記 ----

【MySQL】查詢結果按正體中文排序