Ubuntu 22.04 LTS から Ubuntu 24.04 LTS にアップグレードメモ

Ubuntu 24.04 LTS リリースからしばらく経ちましたが、ようやくメインで使っている ThinkPad P14s (AMD) を Ubuntu 22.04 から 24.04 にアップグレードしました。導入メモ。

アップグレード:

sudo do-release-upgrade

30分ほどで特に問題なく終了。

uname -a
Linux thinkpad-p14s 6.8.0-48-generic #48-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 27 14:04:52 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

導入後の Ubuntu 24.04 LTS (Wayland セッション):

Good work! 😀

無効になったサードパーティリポジトリを再有効化:

以下に手順があります。

Firefox は標準の snap 版ではなく Mozilla 提供のリポジトリのがお勧めです。

GNOME Shell Extention 再有効化:

sudo apt install gnome-shell-extension-manager
  • Clipboard Indicator (クリップボードマネージャ)
  • Current Monitor Window/App Switcher (マルチモニタや仮想画面で Alt(Super) + Tab の出力をカレント画面のアプリだけに抑止)

ふと今気がついたのですが、クリップボードマネージャが画像にも対応していました。グッド。

オーディオ系:

Pipewire の設定で USB-DAC の 96KHz をデフォルトとかにする場合。

  • /usr/share/pipewire/pipewire.conf
  • /usr/share/pipewire/pipewire-pulse.conf
context.properties = {
    ## ..snip..
    default.clock.rate          = 96000
    default.clock.allowed-rates = [ 48000, 96000 ]
    ## ..snip..
}

接続確認。

pw-dump | jq '.[] | select(.type == "PipeWire:Interface:Node" ) | .info.props'

Audacious 出力サンプリングレート設定:

プラグイン – エフェクト – サンプリング周波数コンバータより。(96KHz とかにアップサンプリングする場合)

Wayland 系メモ:

Firefox のピクチャーインピクチャーがデフォルトで最前面表示にならない。右クリックして「最前面に維持する」でうまくいく。

  • OBS Studio の自画面キャプチャはまだうまくいかない。(ので使う場合は X.org セッションに切り替えて使用)
  • 画面キャプチャの Frameshot もキャプチャできずに停止してしまうような動作。いったん GNOME Shell 標準のものでがんばる。
  • Shotcut などアプリ起動のスプラッシュ画面(ウインドウ枠なし)が画面センターではなく、左上とかに表示されるようだ。

関連

KORG NTS-1 digital kit mkII で Rust のカスタムオシレータ制作

KORG の組み立てシンセキットである NTS-1 digital kit mkII は、C/C++ の SDK が公開されており、自分でプログラミングしたオシレータやエフェクタを本体に転送して動作させられる、とても面白いシンセサイザーです。

NTS-1 digital kit mkII – PROGRAMMABLE SYNTHESIZER KIT | KORG (Japan)

作って、繋いで、触って、手のひらサイズの強力なシンセサイザーが帰ってきた !! 進化したカスタマイズにより解き放たれるパワー

かねてより、API が公開されていてカスタムソフトシンセが発音できるハードウェアが欲しいなと考えていましたので、購入して遊んでみました。

PC との接続

本機には USB-C ポートがあり、PC からは MIDI IN/OUT デバイスとして見えます。

ユーティリティとして提供されている KORG KONTROL でカスタムオシレータやエフェクタの転送が MIDI SysEx 経由で行えます。

なお、Windows 接続で他に多くの MIDI 機器が接続されている環境では、KORG MIDI ドライバーが上位の方にいないと、ファームウェアアップデートや KORG KONTROL から本機を認識できないと思われる事象がうちの環境ではありました。(いったん他の MIDI ポートドライバを削除して入れ替えるなどしていたら修正されました)

また、公式の SDK (louge-sdk) で提供されているコマンドライン版の louge-cli は本機には未対応のようです。もしサウンドユニットの開発でコマンドライン転送が使いたい場合は、Python によるオープンソース実装の py-logue-tools を使わせていただくと便利です。

https://github.com/TarkanAl-Kazily/py-logue-tools

Implements a basic python-based MIDI librarian for the Korg NTS-1 Mk2 and similar family of Korg logue-sdk devices. This project is not affiliated with Korg.

サウンドユニットの開発

前述の louge-sdk の README 通りに環境設定し、API に従い C/C++ プログラミングして、できたサウンドユニットを USB-C 経由で転送して試す、というワークフローでうまくいくと思います。

ファームウェアの Ver1.2 未満であると API にいくつか不具合がありますのでアップデートしてからの開発がお勧めです。

本機のサウンドユニットの特徴としては、バイナリに ELF 形式を採用していて USB-C 転送後にダイナミックにユニットをロードしていることです。また、おそらく API のシステムコール先は Arm7 の短縮命令(Thumb)が用いられています。

このことから、普通に SDK でプログラミングしている分には遭遇しないかとは思いますが、いくつかのトラブルシューティングがありますので、ここに列挙しておきます。

音源 Import 時に KORG KONTROL が「ELF が間違っています」エラーでサウンドユニットを受け付けてくれない:

  • ELF バイナリから動的リンクをするダイナミックリンクテーブル rel.ptl エントリに、NTS-1 以外のシステムコールが入っている。
  • SDK の sysroot に入っていない関数がリンクされるとこの状態になります。Linux などに含まれる readelf -a コマンドで中を確認して呼び出しをなんとか解決してあげます。
  • また、ELF ヘッダーの先頭のマジックフラグも KORG KONTROL がバリデートしているようです。

音源 Import から転送までうまくいくもののサウンドユニットが認識されない:

  • 正常に初期化できないユニットは無効にされるようです。KORG KONTROL の状態と不一致して、その後の処理もおかしくなるようなので、いったん本機も KORG CONTROL もリセットしましょう。
  • なお本機のリセットは DELAY ボタンを押しながら電源投入です。(その後の操作はマニュアル参照のこと)
  • header.c で定義する unit_header 値がおかしい。例えば設定パラメータ数 num_params: 2, 項目を 2未満にすると無効になるようです。
  • 転送直後のサウンドウニットの初期化がうまくいかないことがある?
    いったん別ユニットを選択して、自分のユニットに再度設定すると初期化コードが走ったような経験があります。(気のせいかもです)

Rust によるサウンドユニットの制作

C/C++ でかけばいいのですが、完全に趣味、勉強もかねて Rust でつくってみました。リンカー部分でいろいろはまって時間がかかりましたが、以下のリポジトリにビルド手順やソースコードとサウンドユニットのバイナリを配置しています。

https://github.com/h1romas4/nts1mkii-rust-wave

This repository is a Rust ported build of the WAVES(osc) synthesizer for the NTS-1 digital kit mkII included in logue-sdk. If you are interested in creating an NTS-1 Digital Kit mkII sound source in Rust, perhaps this repository may be of some use to you.

README には新しいユニットの追加方法や、はまった部分をメモしてありますので、気になる方はご覧ください。

SDK のサンプルで入っている Waves オシレータをフィルター部分を除いて移植したものと、試しにつくってみた 32byte * 32 音色の波形メモリーオシレータを入れています。

プログラムは C/C++ SDK が提供する関数を bindgen でバインドする方式をとって動作しています。

SDK がインライン展開指定の関数が多いため、Rust 版は速度的、バイナリサイズ的にも若干不利になっており、また no_std 環境で unsafe コード多数で、Rust である意味あるかと言われると現状そうでなさそうな気もしますがw、いったん動くことの勇気として。

ESP32 で WebAssembly Micro Runtime (WAMR) を動かす

WebAssembly Advent Calendar 2023 11 日目の記事です…!

WebAssembly Micro Runtime (WAMR) は C言語で実装された WebAssembly ランタイムのひとつで、Bytecode Alliance が開発しています。

https://github.com/bytecodealliance/wasm-micro-runtime

WebAssembly Micro Runtime (WAMR) is a lightweight standalone WebAssembly (Wasm) runtime with small footprint, high performance and highly configurable features for applications cross from embedded, IoT, edge to Trusted Execution Environment (TEE), smart contract, cloud native and so on. It includes a few parts as below:

引用にあるとおり、小さなフットプリントで動作することからマイコンでも WebAssembly を動作させることができ、おなじみ M5Stack などで採用されている ESP32 SoC もターゲットのひとつとなっています。

本記事では M5 が販売している M5Stamp C3 (ESP32C3/RISC-V) で WARM を動作させた様子を紹介したいと思います。なお、M5Stack (ESP32/Xtensa) と M5Stamp C3 は esp-idf ツールチェインが共通ですので、LCD 制御部分に僅かな修正を入れれば M5Stack でも簡単に動作するはずです。

ESP32C3 のスペック: クロック周波数 160MHz、SRAM: 400KB

AssemblyScript で 3D Cube デモを作成

マイコン上で動作させるプログラムとして AssemblyScript を使った 3D Cube のデモを作成しました。

AssemblyScript は WebAssembly 向けに開発されている TypeScript-like な言語で、コンパイラとして動作し .wasm バイナリーを直接出力します。

https://www.assemblyscript.org/

AssemblyScript targets WebAssembly’s feature set specifically, giving developers low-level control over their code.

バイナリサイズが非常にコンパクトであること(3D Cube で 8KByte弱)からマイコンのようなリソースの限られたコンピュータにも向いており、また GC のタイミングを Embedder が制御できるような export を出力することができます。

AssemblyScript には Math ライブラリも備わりますので、3D Cube のプログラムのメイン部分は次のようなイメージになります。(立方体 8点の行列演算です)

tick(): void {
    const angle: f32 = this.angle;

    const cos: f32 = Mathf.cos(angle);
    const sin: f32 = Mathf.sin(angle);

    const rotX: f32[][] = [
        [    1,    0,     0],
        [    0,  cos,  -sin],
        [    0,  sin,   cos]
    ];
    const rotY: f32[][] = [
        [  cos,    0,  -sin],
        [    0,    1,     0],
        [  sin,    0,   cos]
    ];
    const rotZ: f32[][] = [
        [  cos, -sin,     0],
        [  sin,  cos,     0],
        [    0,    0,     1]
    ];

画面描画部分では次のように Embedder 側の関数を呼ぶようにします。

export declare function draw_line(
    x0: i32, y0: i32, x1: i32, y1: i32, color: i32): void;

connect(i: u32, j: u32, k: u32[][], color: c3dev.COLOR): void {
    const a = k[i];
    const b = k[j];
    c3dev.draw_line(a[0], a[1], b[0], b[1], color);
}

完成した .wasm の export/import は以下のようになります。

  (import "c3dev" "draw_line" (func (;0;) (type 8)))
...snip...
  (export "init" (func 12))
  (export "tick" (func 19))
  (export "__new" (func 6))
  (export "__pin" (func 20))
  (export "__unpin" (func 21))
  (export "__collect" (func 22))

export で __ 付きになっている関数は AssemblyScript ランタイムの関数で、GC に関する制御を行えるものです。これは --exportRuntime コンパイルオプションで指定すると export されます。

asc src/main.ts
  -o ../../resources/wasm/3dcube.wasm
  -t build/3dcube.wat
  -b build/3dcube.d.ts
  --runtime minimal
  --exportRuntime
  --lowMemoryLimit 32767
  --use Math=NativeMathf
  --use abort=
  -O3
  --optimize

また --runtime オプションでは GC の動作を指定できます。ここでは任意のタイミングで GC を行いたかったため minimal を指定しています。

--runtime Specifies the runtime variant to include in the program.
incremental  TLSF + incremental GC (default)
minimal      TLSF + lightweight GC invoked externally
stub         Minimal runtime stub (never frees)

AssemblyScript 3D Cube のソースコードは次のリポジトリに置きました。draw_line を Web API の Canvas に繋いでいますので、npm install && npm run start にてウェブブラウザで動作確認できます。

https://github.com/h1romas4/m5stamp-c3wamr/tree/main/wasm/3dcube

WebAssembly Micro Runtime を esp-idf でビルド

WebAssembly Micro Runtime は ESP32 のツールチェインである esp-idf 向けのビルドスクリプトを持っています。

このことからビルドは簡単で、 esp-idf のディレクトリストラクチャで新規プロジェクトを作成し、components ディレクトリに WAMR を submodule add するなどした後、CMakeLists.txt で次のようにすればセットアップ完了です。

# wasm-micro-runtime settings
list(APPEND EXTRA_COMPONENT_DIRS "components/wasm/wasm-micro-runtime/build-scripts/esp-idf")
set(WAMR_BUILD_INTERP 1)
set(WAMR_BUILD_FAST_INTERP 1)
set(WAMR_BUILD_AOT 0)
set(WAMR_BUILD_LIBC_BUILTIN 1)
set(WAMR_BUILD_APP_FRAMEWORK 0)
idf_component_register(
    INCLUDE_DIRS ${INCLUDEDIRS}
    SRCS ${SRCS}
    REQUIRES arduino-esp32 lcd wamr #←追加
)

update (2024-07-21):

WARM 2.1 系より esp-idf 向けの kconfig が加わり、上記の設定は CMakeLists.txt ではなく menuconfig から行えるようになりました。

idf.py menuconfig
-> Component config -> WASM Micro Runtime

WebAssembly C and C++ API

WAMR は WebAssembly ランタイムを C/C++ からロードするために標準化が進められている WebAssembly C and C++ API に対応しています。

https://github.com/WebAssembly/wasm-c-api

Provide a “black box” API for embedding a Wasm engine in other C/C++ applications.

この仕様は、様々な WebAssembly ランタイムごとに異なっているランタイムインスタンスの作成や export/import のバインドの仕方を WebAssembly C and C++ API で統一化しようという動きです。

ということで、WebAssembly C and C++ API な #include “wasm_c_api.h” を使った場合、ランタイムのロードや設定は次のようになります。(参考:https://github.com/WebAssembly/wasm-c-api/blob/main/example/hello.c

なお、WAMR は mutex を pthread API で取得して動作するため、esp-idf 上で動作させる場合は pthread をひとつ立てた中で実行してください。

ランタイムイニシャライズ:

// Initialize.
ESP_LOGI(TAG, "WAMR Initialize...");
wasm_engine_t* engine = wasm_engine_new();
wasm_store_t* store = wasm_store_new(engine);

.wasm ロード:

// Load binary.
ESP_LOGI(TAG, "Load Wasm binary...");
SPIFFS_WASM.begin(false, "/wasm", 4, "wasm");
File wasm_file = SPIFFS_WASM.open("/3dcube.wasm", "rb");
size_t file_size = wasm_file.size();
ESP_LOGI(TAG, ".wasm size: %d", file_size);
// load wasm binary
wasm_byte_vec_t binary;
wasm_byte_vec_new_uninitialized(&binary, file_size);
if(wasm_file.read((uint8_t *)binary.data, file_size) != file_size) {
    ESP_LOGE(TAG, "Error loading module!");
    return NULL;
}
// close file
wasm_file.close();
SPIFFS_WASM.end();

.wasm バリデート・コンパイル

// Validate.
ESP_LOGI(TAG, "Validating module...");
if (!wasm_module_validate(store, &binary)) {
    ESP_LOGE(TAG, "Error validating module!");
    return NULL;
}

// Compile.
ESP_LOGI(TAG, "Compiling module...");
own wasm_module_t* module = wasm_module_new(store, &binary);
if (!module) {
    ESP_LOGE(TAG, "Error compiling module!");
    return NULL;
}

.wasm import のコールバック作成(ここで draw_line 5引数関数からマイコン LCD 描画を呼び出します)

// Create external functions.
ESP_LOGI(TAG, "Creating callback...");
// (import "c3dev" "draw_line" (func (;1;) (type 8)))   draw_line_type
own wasm_functype_t* draw_line_type = wasm_functype_new_5_0(
    wasm_valtype_new_i32(),
    wasm_valtype_new_i32(),
    wasm_valtype_new_i32(),
    wasm_valtype_new_i32(),
    wasm_valtype_new_i32()
);
own wasm_func_t* draw_line_func = wasm_func_new(store, draw_line_type, draw_line_callback);
wasm_functype_delete(draw_line_type);
/**
 * draw_line_callback (A function to be called from Wasm code)
 */
own wasm_trap_t* draw_line_callback(
    const wasm_val_vec_t* args, wasm_val_vec_t* results
) {
    tft.drawLine(
        args->data[0].of.i32,
        args->data[1].of.i32,
        args->data[2].of.i32,
        args->data[3].of.i32,
        args->data[4].of.i32
    );
    return NULL;
}

.wasm export のバインド(ここで .wasm 側の init、tick__collect を取得して Embedder から呼び出せるようにします)

// (export "init" (func 14))        init_func
// (export "tick" (func 21))        tick_func
// (export "__new" (func 8))        -
// (export "__pin" (func 22))       -
// (export "__unpin" (func 23))     -
// (export "__collect" (func 24))   collect_func
const wasm_func_t* init_func = wasm_extern_as_func(exports.data[0]);
if(init_func == NULL) {
    ESP_LOGE(TAG, "Error accessing export!");
    return NULL;
}
const wasm_func_t* tick_func = wasm_extern_as_func(exports.data[1]);
if(tick_func == NULL) {
    ESP_LOGE(TAG, "Error accessing export!");
    return NULL;
}
const wasm_func_t* collect_func = wasm_extern_as_func(exports.data[5]);
if(collect_func == NULL) {
    ESP_LOGE(TAG, "Error accessing export!");
    return NULL;
}

.wasm アプリケーション開始。 init 関数(LCD解像度 2引数)を呼び出し:

ESP_LOGI(TAG, "Calling export...");
wasm_val_t as[2] = { WASM_I32_VAL(160), WASM_I32_VAL(128) };
wasm_val_vec_t args = WASM_ARRAY_VEC(as);
wasm_val_vec_t results = WASM_EMPTY_VEC;
// init
if (wasm_func_call(init_func, &args, &results)) {
    ESP_LOGE(TAG, "Error calling function!");
    return NULL;
}

アプリケーションループ。 wasm 側の tick と __collect (GC) 関数の呼び出し(Wasm tick 関数が 3D Cube の座標計算を行い draw_line することで実際にマイコン側に描画されます)

// loop 10000 frame
wasm_val_vec_t args_empty = WASM_EMPTY_VEC;
wasm_val_vec_t results_empty = WASM_EMPTY_VEC;
for(size_t i = 0 ; i < 10000; i++) {
    // tick
    if(wasm_func_call(tick_func, &args_empty, &results_empty)) {
        ESP_LOGE(TAG, "Error calling function!");
        return NULL;
    }
    // GC for AssemblyScript
    if(wasm_func_call(collect_func, &args_empty, &results_empty)) {
        ESP_LOGE(TAG, "Error calling function!");
        return NULL;
    }
    delay(1); // for watch dog timer
}

デモンストレーション

WebAssembly Micro Runtime で 3D Cube を動作させた様子です。動作クロック 160MHz のインタープリタ Wasm として十分実用可能な速度と感じました。

実行中の残ヒープメモリは 176KB とこちらもまだまだ余裕がありそうです。

I (307) main.cpp: heap_caps_get_free_size: 320940
I (1670) main.cpp: heap_caps_get_free_size: 176924

マイコン WebAssembly は import/export のインターフェースさえ実装できれば、ウェブブラウザー上でさくさくつくって、時折数キロの .wasm をマイコンに転送して動作確認するというイテレーションで開発できるようになるので便利です。(この記事のプロジェクトでも設定していますが、ESP32 のフラッシュパーティションを .wasm 専用にコンパクトに切ると PC からの転送が数秒で終わります

WARM は簡単に動作させられますので、何かマイコンを見つけたら動作させてみると面白いかもしれません。

組み込み向け WebAssembly としては、WASI において Digital I/O(GPIO)、I2C、SPI のインターフェースが Phase 1 に上がっていてこちらの進展も楽しみです。

https://github.com/WebAssembly/WASI/blob/main/Proposals.md#phase-1—feature-proposal-cg

この記事のソースコードリポジトリ

https://github.com/h1romas4/m5stamp-c3wamr

This repository demonstrates the use of wasm-micro-runtime on M5Stamp C3 to run WebAssembly applications.

M5Stack Core2 (Xtensa 240MHz)バージョン

https://github.com/h1romas4/m5stamp-c3wamr/tree/target-m5stack

関連