2018年11月26日 星期一

WAV檔案格式學習心得

WAV檔案格式學習心得

前言

  最近接合Audio API到遊戲引擎上,但Audio API都不提供Parse檔案相關的功能,所以必須要自己Parse後萃取出raw data,再將raw data送入Audio API。以下是學習心得,在此做個紀錄。

內容

  WAV檔的格式是由很多的RIFF chunk所組成,檔案組成的概況如下圖
WAV檔的組成
每個RIFF chunk裡都包含了RIFF header與Content,每個RIFF header會用chunkID來表明資料的型態,而size所表明的是RIFF chunk裡的Content資料有多少Bytes。在Parse時,常常需要知道現在的RIFF chunk有多少Bytes,計算法如下
size_t riffChunkSize=riffHeader.size+sizeof(CRIFFHeader);

  接著來看看WAV檔裡的Chunk的Content,Content有多少種呢?不幸的很多種,所以這裡只列出我有遇到的種類。
chunkID是個32位元的資料,也就是4個Bytes,通常會將每個Byte表達成char,這樣就可以4個char來表達資料種類。

  第一種的chunkID為'R','I','F','F',這個很特別,它通常會是第一個RIFF chunk,特別的是它的content,一定是一個int32_t,且內容是'W','A','V','E',但是它的RIFF header裡的size的內容並不是4,而是整個WAV檔的大小減8,這個8其實就是RIFF header的大小,有點類似資料夾的概念,這個chunk包住了其它的chunk。

  第二種的chunkID為'f','m','t',' ',這裡的Content可以參考WAVEFORMATEX structure,但要注意struct裡的"cbSize",這個欄位不一定存在,所以這個chunk的size不是固定的!請依據chunk裡的size來Parsem。

  第三種的chunkID為'f','a','c','t',這個chunk的內容我並不知它的Content是什麼,我選擇直接跳過它!請注意這個chunk並不是一定存在,parse時請考慮這點。

  第四種的chunkID為'd','a','t','a',這裡就是raw data,也就是Audio API吃的資料。

  最後把前述的種類排列起來就會像下圖
Wav檔的Chunk排列

參考資料

WAVE PCM soundfile format
WAVEFORMATEX structure

2018年11月19日 星期一

ndk-build的使用心得

ndk-build的使用心得

前言

  最近將繪圖引擎跨到Android,在繪圖API的部分會面臨到選擇GLES的版本的問題,還有像是GLES3只有64位元支援,如果只靠修改Android.mk與Application.mk的話會沒辦法做出像是組態(Configuration)的效果,所以對ndk-build做了研究,在此做個紀錄。

內容

  ndk-build本身不提供組態(Configuration)的功能,但可以透過參數指定Android.mk與Application.mk,這做法可以做出組態(Configuration)的效果,但只要想想組態越多,Android.mk與Application.mk的數量也會隨著組態而增多,GLES的版本目前有GLES2、GLES3、GLES3.1與GLES3.2,如果用這個方法就會有8個檔案要維護,加上共用的部分還要隔開的成本,做了一下我就放棄這個做法,因為成本有點高。

  在google一陣子後發現ndk-build可以直接從命令列新增或修改在Android.mk與Application.mk的參數,比方說"APP_ABI"這個參數,在入門教學中會把該參數寫在Application.mk,用來決定要建置的ABI,但事實上是可以用ndk-build的命令列來修改,命令如下
ndk-build APP_ABI=arm64-v8a

當用這個命令後,請移除Application.mk裡的"APP_ABI"的設定,不然ndk-build社的數值會被覆蓋,如果要設定多個參數就用空白隔開即可。只靠這個方法來實現組態會不太夠,必須再搭配makefile的if語法,語法的範例如下
ifeq ($(GLES_VERSION),GLES2)
GLES_LIBS := -lGLESv2
endif
ifeq ($(GLES_VERSION),GLES3)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_1)
GLES_LIBS := -lGLESv3
endif
ifeq ($(GLES_VERSION),GLES3_2)
GLES_LIBS := -lGLESv3
endif

範例中使用"GLES_VERSION"這個變數來決定最後的要載入的LIB,"GLES_VERSION"是自訂的,搭配之前的方法可以在ndk-build的命令列隨喜好來修改,這樣不論是選GLES版本或者是只能建64位元版本都可以解決了!

  ndk-build在指定ABI的版本來建置後,會把非建置版本的ABI移除,例如APP_ABI=arm64-v8a時,x86_64、armeabi-v7a與x86都會被移除,這個問題目前解決的方案不是很好,就是每個ABI都分不同資料夾來建置,可以在ndk-build的命令列修改"NDK_OUT"與"NDK_LIBS_OUT"來完成,這個方法可行,但就是覺得不太優雅,有機會再找找有沒有優雅的解決方案。

  ndk-build的clean有點討厭,它只為清除LIB,但不會清除OBJ,如果需要完整重建的話,就手動清空或是寫個命令來清除。

  最後來說一下命令的部分,我個人是使用Python來執行ndk-build並選擇組態(Configuration),不選擇Shell與bash只是單純地不熟,但Python雖然跨平台,但有新舊版本的問題(Python2.7與Python3),這部分的實現目前就這樣解決,如果有更好的方法以後再來說明。

參考資料

Application.mk
ndk-build

2018年11月12日 星期一

在Android裡開啟Dialog

在Android裡開啟Dialog

前言

  在之前的這篇"在Android實現多個按鍵同時按壓"有提及要做一個輸入Console命令的視窗,該篇說到了開啟子視窗的方法,這篇來實作UI的部分,在此做個紀錄。

內容

  Android並沒有視窗UI,而"Dialog"的功能可以做出像在Windows開子視窗,所以本篇會使用Dialog來實現,在找資料的過程中,還找到了也可以做出類似效果的"AlertDialog",所以方法並不是只有一種,有機會再來說明"AlertDialog"。

  開啟一個"Empty activity"的專案,接著要新增一個"Layout",具體的操作如下圖
在layout資料夾新增xml
在layout資料夾點擊右鍵後,依據上圖的操作新增xml,接著會看到下圖
新增layout的設定視窗
看到新增layout的設定視窗後,更改layout的名稱,本篇使用"sub_dialog",可以自己命名,但要注意不能有大寫的文字。新增完後開啟該xml檔,依據下圖操作
修改layout的內容
依據上圖開啟layout的文字模式,輸入以下xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="Name" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Close" />

</LinearLayout>


這個xml會規劃一個輸入的text與一個關閉的按鈕。

  接著在activity的layout新增一個按鈕,用來開啟Dialog,操作如下圖
新增開啟Dialog的按鈕
按鈕增加完後,接著在Activity裡開啟,範例碼如下
package com.hosee.hellodialog;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.app.Dialog;
import android.util.Log;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {
    Dialog subDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btnOpenDialog = (Button)findViewById( R.id.button2);
        btnOpenDialog.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                subDialog=new Dialog(MainActivity.this);
                subDialog.setTitle("Title");
                subDialog.setCancelable(false);
                subDialog.setContentView(R.layout.sub_dialog);

                Button btnClose=(Button)subDialog.findViewById(R.id.button);
                btnClose.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        EditText editText=(EditText)subDialog.findViewById(R.id.editText);
                        Log.i("HelloDialog","Input:"+editText.getText() );
                        subDialog.cancel();
                    }
                });
                subDialog.show();
            }
        });
    }
}


範例碼有關於View的ID可能會與範例碼的ID不同請依據layout的命名來修改。在"btnOpenDialog"就是Activity用來開啟Dialog的按鈕,為它設定"Click"事件,在"Click"事件中,會看到開啟Dialog的過程,setTitle()可以設定Title的顯示名稱,似乎不能省略,setCancelable()設為false表示當使用者點擊Dialog是窗外的範圍時"不會"關閉Dialog,接著會看使用"subDialog"來執行findViewById(),而不是直接用activity的findViewById(),這裡要注意!然後設定關閉按鈕的Click"事件,這裡會再關閉前列印輸入text的內容後執行"subDialog.cancel()",這道命令會真正的關閉Dialog,在將事件都設定完後可以看到"subDialog.show()",這道命令會開啟Dialog,完成的結果如下圖
執行結果


參考資料


相關文章

在Android實現多個按鍵同時按壓

2018年11月5日 星期一

在Android實現多個按鍵同時按壓

在Android實現多個按鍵同時按壓

前言

  由於繪圖引擎跨到Android時是全螢幕狀態,加上考慮接合Android的事件如Click、Touch...等,這時多了個麻煩,要如何輸入Debug的命令?在PC不論是Window或Linux都有Console視窗,但Android卻沒有,所以目前想到的是在螢幕的左上與右下新增隱形的按鈕,當同時壓下時會出現UI可以輸入命令。這次只記錄同時按壓的部分,在此做個紀錄。

內容

  先在Android studio裡開一個"Empty activity"的專案,開完後到Layout裡新增2個TextView,如下圖
在Layout新增按鍵
按鍵是用TextView,而不是Button,因為TextView當裡面的text是空值時就可以變成隱形的按鈕,範例為了演示而未清空。要注意圖中右邊的ID與對其都要調整,ID的部分為textView1與textView2,而對齊分別為左上與右下。

  調整為Layout後,接著就來偵測同按壓時,範例如下
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.util.Log;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
    private long time1=0;
    private long time2=0;
    private void CheckMultiClick(){
        long delTime=time1-time2;
        if(delTime < 0)
            delTime*=-1;
        //
        if(delTime<1000){
            Log.i("MultiClick","Double click!");
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv1=(TextView)findViewById(R.id.textView1);
        TextView tv2=(TextView)findViewById(R.id.textView2);
        //
        tv1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                time1=(new Date() ).getTime();
                CheckMultiClick();
            }
        });
        //
        tv2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                time2=(new Date() ).getTime();
                CheckMultiClick();
            }
        });
    }
}

範例中,先分別為TextView上OnClick事件,可以看到當Clcik發生時會記錄"time",而這個"time"的意義可以參考Java日期時間(Date/Time),紀錄完後喚起"CheckMultiClick()",裡面很簡單,將兩個"time"的差做檢查是否小於1000,這裡的時間差代表兩個"time"差幾毫秒(milliSecs),所以1000代表差1秒就發生同時按壓。

參考資料

Java日期時間(Date/Time)