LoginSignup
9

More than 5 years have passed since last update.

いまどきのシンセサイザをPerlで弄る -Perl meets KRONOS-

Posted at

はじめに

いまどきのシンセサイザと書きましたが今回もKORG KRONOSが対象です。
Perlで直接KRONOSを弄るわけではないですが、MIDIシステムエクスクルーシブを利用して弄ります。
今回はKRONOSのCombinationの音色パッチを編集します。その時のメモ。
簡単な処理の流れは以下の通りです。

KRONOS

MIDIシステムエクスクルーシブでダンプ

Perlで解析、編集

MIDIシステムエクスクルーシブでリストア

KRONOS

また、これらに関連するハード、ソフトの準備、ドキュメントの読み方も解説します。

そもそもシステムエクスクルーシブとは

MIDIメッセージの中でステータスバイトが0xF0から始まり任意の数のデータバイトが続いた後
0xF7で終わるメッセージのことです。
呼んで字のごとく各メーカがそれぞれ排他的にデータバイトのフォーマットを決めることが出来ます。
システムエクスクルーシブを使えばシンセの音色の編集や本体の操作等が出来ます。
もちろんKRONOSもシステムエクスクルーシブに対応しています。

準備

ハード
  • KRONOS X
  • Mac
ソフト
ドキュメント

KRONOS システム・バージョン2.1.1はKRONOS本体に、それ以外はMacにインストールします。
PerlはMacにデフォルトで入っているものを使用しています。
バージョンは参考までに。

KRONOSからMIDIシステムエクスクルーシブでダンプ

KRONOSとMacをUSBケーブルで接続します。MIDIケーブルとMIDIインターフェースを使っても良いですが
USBケーブルの方が高速に、双方向に通信できるので特に理由がなければUSBをおすすめします。

KRONOSを起動させてGlobalモード->MIDIタブに遷移します。
PageMenuのDump Combinationを選びます。

k1.jpg

CombinationのU-A000のパッチをSingleでダンプします。
(今回はわかりやすいように初期状態のパッチの名前だけ'HOGEHOGEHOGEHOGE'に変更したものを使用)

KRONOS側のOKを押す前にMac側の準備をします。
MIDIメッセージを取り込んでファイルに保存するためにSysEx Librarianを使います。
SysEx Librarianを起動させてRecord Oneをクリック。

s2.png

この状態でKRONOS側のOKボタンを押すと自動的にMacにMIDIシステムエクスクルーシブが
".syx"形式で保存されます。(保存先はPreference->General->Sys Ex Folder Locationで確認可)

s3.png

.syxを覗く

保存したファイルの中身を確認しないことには始まらないので、
.syxファイルをバイナリエディタ(0xED)で開きます。

b1.png

MIDIメッセージを収めたファイルですが、SMF(Standard MIDI File)形式に見られる
ヘッダチャンクの16進数の「4D 54 68 64」がありません。代わりに「F0 42 30 68」とデータ続いています。
見ての通りシステムエクスクルーシブの保存形式".syx"は、MIDIメッセージが
そのまま *バイナリ
で保存されています。
次はPerlでこのファイルを読み込んで編集します。

Perlで.syxを編集する

ソースコード

.syxファイルの解析、編集用に自作したPerlのプログラムは下記リポジトリで公開しています。
https://github.com/kurobeniq/Perl-meets-KRONOS

ディレクトリ構成


.
├── lib
│   └── SysEx.pm
├── sample1.pl
└── syx
    ├── A000.syx
    ├── A000_mod.syx
    └── CombiUAStoreBankRequest.syx

2 directories, 5 files

sample1.plはメインのプログラムで、その中から呼び出すメソッド群は
lib/SysEx.pmにまとめています。(なんちゃってPerlモジュール)
syx/A000.syxはSysEx Librarianで保存したファイルです。
syx/A000_mod.syxはA000.syxの編集後のファイルです。
syx/CombiUAStoreBankRequest.syxはCombination U-Aバンクに対してセーブする命令を送るファイルです。

プログラムを実行します。

./sample1.pl syx/A000.syx

実行するとsyx/A000_mod.syxが作成されます。
これはのCombinationの音色パッチを編集したファイルです。

実際にどんな内容で編集しているのか確認します。


ファイル読み込み
sample1.pl
# syxファイル読み込み
my $filename = $ARGV[0];
my $sysex_msg_str = SysEx::read_from_file($filename);

コマンドライン引数から受け取ったファイル名を読み込みます。

SysEx.pm
sub read_from_file {
    my $filename = shift;
    my $current_sysex_msg_str;
    my $sysex_msg_str;

    open(my $IN, "<", $filename) or die "$!";
    binmode $IN; # Macでは要らないけどWindowsマシンで動くかもしれないので

    while (read($IN, my $val, 1)){
        # 読み込んだMIDIデータバイトを2文字の16進数文字列に変換
        $current_sysex_msg_str = unpack("H2", $val);

        $sysex_msg_str .= $current_sysex_msg_str;

        # sysexの終了を表すメッセージが来たら読み込み終了
        if (lc($current_sysex_msg_str) eq 'f7'){ last }
    }
    close $IN;
    return $sysex_msg_str;
}

$filenameを1バイトずつunpack(元がバイナリなので)しながら読み込んで行きます
1バイトずつ読む理由は0xf7が来たら読み込みを終了するためです。

$sysex_msg_strには'f042306873...以下略'とMIDIメッセージがそのまま入っています。


解析その1

ここからは解析するためにKORGが公開している
MIDIシステム・エクスクルーシブ・メッセージ資料(KRONOS SysEx documentation 2.1)
http://www.gearzonemusic.com/Kronos/KRONOS_SysEx_2_1.zip を見ながら進めます。

KRONOS_MIDI_SysEx.txtによると'f042306873'で始まるシステムエクスクルーシブメッセージは

[73] Object Dump

だそうです。

KRONOS_MIDI_SysEx.txt
[73] Object Dump                                                          Receive/Transmit
        F0, 42, 3g, 68          Excl Header
        73                      Function
        obj                     *1
        bank                    *2
        idH                     Index (bit7-13)
        idL                     Index (bit0-6)
        version                 obj's version number
        data                    *3
        F7                      End of Excl

F0, 42, 3g, 68は固定で'g'の部分はKRONOS本体側で設定しているMIDIチャンネルが入ります。
(今回は1ch = 0が入っています)
73もDump Combinationした際は固定です。
obj以降は見ていきます。

sample1.pl
say "object_type = ".SysEx::get_object_type($sysex_msg_str);
say "bank        = ".SysEx::get_bank($sysex_msg_str);
say "index       = ".SysEx::get_index($sysex_msg_str);
say "version     = ".SysEx::get_version($sysex_msg_str);
SysEx.pm
sub get_object_type { return &_common_get($_[0], 5, 5) }

sub get_bank    { return &_common_get($_[0], 6, 6) }
sub get_index   { return &_common_get($_[0], 7, 8) }
sub get_version { return &_common_get($_[0], 9, 9) }
SysEx.pm
sub _common_get {
    my $sysex_msg_str = shift;
    my $start_ofs     = shift;
    my $end_ofs       = shift;
    return substr($sysex_msg_str, $start_ofs * 2, (1 + $end_ofs - $start_ofs) * 2);
}

それぞれ読み込み開始のオフセットと終了オフセットを指定して取得します。
// オフセットは0スタートです
例えばobjは5バイト目から5バイト目まで、
Index(idH, idL)は7バイト目から8バイト目までを取得します。

取得結果

object_type = 01
bank        = 40
index       = 0000
version     = 03

ドキュメントによるとobject_type = 01はCombination
今回Combinationの音色パッチをダンプしたので合っています。

KRONOS_MIDI_SysEx.txt
*1
        obj = 00 : Program (Prog_EXi_Common.txt, Prog_EXi.txt, Prog_HD-1.txt)
              01 : Combination (CombiAndSongTimbreSet.txt)
              02 : Song Timbre Set (CombiAndSongTimbreSet.txt)
              03 : Global (Global.txt)

bank = 40はUSER-A
U-A000をダンプしたので合っています。

KRONOS_MIDI_SysEx.txt
*2
 The meaning of bank depends on the object type.

 Program, Program Name:
        bank =  0 -  5 : INT-A - F
               10 - 1A : GM, g(1)-g(9), g(d) (read-only)
               40 - 4d : USER-A - G, AA - GG

 Combi, Combi Name:
        bank =  0 -  6 : INT-A - G
               40 - 46 : USER-A - G

index = 0000はU-A 000をダンプしたので合っています。
// U-A127のダンプ時は007Fでした。Combinationは各バンク0-127までしかないので
// 7bitで十分だと思いますが、Sequencerになると0-199まで有るのでがっつり2バイト構成*にして
// 8bit以上の値も表現出来るようにしているのでしょうか。
// ※ MIDIのデータバイトは1バイトで7bitまでしか扱えません。

version = 03に関してはU-A127のダンプ時も03固定でした。


解析その2(7bit->8bitデコード編)

次はdata部を取得します。

sample1.pl
# sysexメッセージからdata部分取得
my $data_str = SysEx::get_data($sysex_msg_str);
SysEx.pm
sub get_data    { return &_common_get($_[0], 10, 8935) }

data部は10バイト目から8935バイト目まで読み込みます。
Combinationの場合、どんなCombinationのパッチでも終了オフセット8935バイト目という値は固定です。
data部にはCombinationの核とも言えるTimberの情報やエフェクトの設定、Combinationのパッチ名等が含まれています。

ただしdata部の解析は一筋縄ではいきません。
ドキュメントのdata部の詳細を見てみると...

KRONOS_MIDI_SysEx.txt
*3
 The detailed information about "data," see the text files specific to HD-1, EXi, etc.
 Internal data is converted to MIDI data using following method.
+----------------------------------------------------------------------------------------+
|  Internal 7byte data <--convert--> MIDI 8 byte data                                    |
|  example) Internal data(bit image) MIDI data(bit image)                                |
|                Aaaaaaaa            0GFEDCBA                                            |
|                Bbbbbbbb            0aaaaaaa                                            |
|                Cccccccc            0bbbbbbb                                            |
|                Dddddddd            0ccccccc                                            |
|                Eeeeeeee            0ddddddd                                            |
|                Ffffffff            0eeeeeee                                            |
|                Gggggggg            0fffffff                                            |
|                Hhhhhhhh            0ggggggg                                            |
|                Iiiiiiii            0NMLKJIH                                            |
|                   :                0hhhhhhh                                            |
|                   :                   :                                                |
|                Vvvvvvvv            000000WV                                            |
|                Wwwwwwww            0vvvvvvv                                            |
|                                    0wwwwwww                                            |
|                                    11110111 (EOX=F7H)                                  |
+----------------------------------------------------------------------------------------+

 binarySize: number of 8-bit binary data bytes in memory
 sysExSize: number of 7-bit sys/ex data bytes

と書いてあります。簡単に説明すると
Internal dataMIDI data の2種類のデータの持ち方があり
Internal data はKRONOS内部で扱っている形式で1バイトあたり8bitフルフル使っています。
MIDI data はMIDIメッセージに Internal data を載せるために1バイトあたり7bitで
表現された形式です。(MIDIメッセージのデータバイトは最上位1bitを0固定にしないといけないため)

Internal data(8bit) -> MIDI data(7bit)に変換する際、
7バイト1組が8バイト1組に変換されます。ここで増えた1バイトは Internal data 時の
7バイト1組のそれぞれの最上位bitを寄せ集めて出来た1バイトです。(0GFEDCBA)
これを元のデータの長さまで繰り返します。結果的にデータ量は8/7倍に増えます。

// 通信経路の制約上、元のbit数より少ないbit数で置き換えて表現するという概念はBase64と似ています。

data部を解析するためには MIDI data -> Internal data に戻して(デコード)する必要があります。

sample1.pl
# data_str(7bit)を$internal_data_str(8bit)にデコード
my $internal_data_str = SysEx::kronos_data_decode($data_str);

SysEx.pm
sub kronos_data_decode {
    my $data_str = shift;
    my $msb_collect_bin_str; # 7bitに落としこむために8bit時のMSBを寄せ集めたbit列
    my $internal_data_str;   # kronos内部で扱っているInternal data形式

    for (my $data_ofs = 0; $data_ofs < length($data_str) / 2; $data_ofs++) {
        # 1byte(16進文字列を2文字)ずつ読み込み
        my $current_sysex_msg_str = substr($data_str, $data_ofs * 2, 2);

        # $current_sysex_msg_strがMSBを寄せ集めたbit列を表す16進文字列の時
        # ドキュメントの情報では$data_ofsが8の倍数毎に出現する
        if ($data_ofs % 8 == 0){
            # 2進文字列に変換
            $msb_collect_bin_str = &_hex2bin($current_sysex_msg_str);
        } else {
            # 今読み込んでいる'$data_ofs % 8'の'値'に応じて
            # 直近の$msb_collect_bin_strから'n'bit目を取得し$MSBに格納
            my $MSB = substr($msb_collect_bin_str, -1 * $data_ofs % 8, 1);

            my $current_sysex_msg_bin_str = &_hex2bin($current_sysex_msg_str);

            # 先頭1bitを$MSBで置き換える
            substr($current_sysex_msg_bin_str, 0, 1, $MSB);

            $internal_data_str .= &_bin2hex($current_sysex_msg_bin_str);
        }
    }
    return $internal_data_str;
}

ざっくり説明すると$data_strを1バイトずつ読み込み
7バイト1組のそれぞれの'最上位bitを寄せ集めて出来た1バイト'が来た時は変数に一旦保存しておき、
それ以外のバイトが来た時に最上位bitを0or1にしていきます。

// この辺のロジックはビットシフト、マスク、論理和で置き換えれば高速化出来そうですね


解析その3(data部解析)

$internal_data_strには'484f4745484f4745484f4745484f474500000...'という16進数文字列で
表現された値が7810バイト分(16進数文字列で表現してるのでその倍)入っています。

Internal data 形式のフォーマット情報は
MIDIシステム・エクスクルーシブ・メッセージ資料(KRONOS SysEx documentation 2.1)に含まれる
SysExDumps/CombiAndSongTimbreSet.txtを見ます。

CombiAndSongTimbreSet.txt
Combi and Song Timbre Set objects have the same structure.

Combination Size: 7810 byte
Object Version: 3
+======+=====+=======================+============================================+==============+====================+===+===+===+===+===+
| OFS  | bit | parameter             | parameter                                  | data(hex)    | value              |TYP|SOC|SUB|PID|IDX|
+======+=====+=======================+============================================+==============+====================+===+===+===+===+===+
|    0 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    1 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    2 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    3 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    4 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    5 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    6 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    7 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    8 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|    9 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   10 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   11 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   12 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   13 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   14 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   15 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   16 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   17 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   18 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   19 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   20 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   21 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   22 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
|   23 |     | Common                | Name                                       | 00~FF        |  ~                 |   |   |   |   |   |
+======+=====+=======================+============================================+==============+====================+===+===+===+===+===+

Internal data 形式の0-23バイト目はCombinationの名前が入っているので抽出します。

sample1.pl
# internal_data_strからname(16進数文字列を取り出し)
my $name_str = SysEx::get_name_by_internal_data_str($internal_data_str);

# internal_data_str形式から表示名を取得
my $original_name_str = SysEx::name2original_name($name_str);

say "original_name_str = ".$original_name_str;
SysEx.pm
sub get_name_by_internal_data_str { return &_common_get($_[0], 0, 23) }
SysEx.pm
sub name2original_name {
    my $name_str = shift;
    my $original_name_str;
    for(my $ofs = 0; $ofs < length($name_str) / 2; $ofs++){
        my $msg = substr($name_str, $ofs * 2, 2);
        # '00' (NUL文字)がきたら終了
        if ($msg eq '00'){ last };

        $original_name_str .= pack("H2", $msg);
    }
    return $original_name_str;
}

$name_strの時点ではASCIIコード値が入っているだけなので、ASCIIコード値に該当する
元の文字列に戻します。

取得結果

original_name_str = HOGEHOGEHOGEHOGE

ようやくCombinationの名前も取得することが出来ました。

ついでにInsert Effect1のEffect Typeも取得します。

sample1.pl
# internal_data_str形式からinsert_effect1情報取得
my $insert_effect1_str = SysEx::get_insert_effect1_by_internal_data_str($internal_data_str);

# insert_effect_strからeffect_type_dec_str(0-185)取得
my $effect_type_dec_str = SysEx::insert_effect2effect_type($insert_effect1_str);

say "effect_type_dec_str = ".$effect_type_dec_str;
SysEx.pm
sub get_insert_effect1_by_internal_data_str { return &_common_get($_[0], 88, 96) }
SysEx.pm
sub insert_effect2effect_type {
    my $effect_type_str = &_common_get($_[0], 0, 0);
    # 10進にして返す
    return hex($effect_type_str);
}

取得結果

effect_type_dec_str = 0

初期状態のパッチなので当たり前ですが0です(No Effect)


編集、.syx書き出し

さて、ここまで来たら後は名前とEffect Typeを編集して
今まで解析でやってきた逆のことをしていくだけです。

sample1.pl
# コンビネーションの名前を編集
$original_name_str .= '_MOD';

# original_name_strをinternal_data_str形式にする
$name_str = SysEx::original_name2name($original_name_str);

# internal_data_strにnameをセット
$internal_data_str = SysEx::set_name_to_internal_data_str($internal_data_str, $name_str);

# インサートエフェクト1のeffect_typeを140でセット
$insert_effect1_str = SysEx::effect_type2insert_effect($insert_effect1_str, 140);

# internal_data_strにinsert_effect1をセット
$internal_data_str = SysEx::set_insert_effect1_to_internal_data_str($internal_data_str, $insert_effect1_str);

# data部分をエンコード
$data_str = SysEx::kronos_data_encode($internal_data_str);

# sysexメッセージにdata部分をセット
$sysex_msg_str = SysEx::set_data($sysex_msg_str, $data_str);

# ファイルに書き出し
$filename =~ s/.syx$/_mod.syx/;
SysEx::write_to_file($filename, $sysex_msg_str);

(SysEx.pm内のメソッド側の紹介はここでは長いので省略。githubを参照してください)

殆どのメソッドは解析時に使用したget系をset系にしたりファイル読み出しが書き出しに変わったりしてるだけですが
SysEx::kronos_data_encodeはデコード時には不要だった先読みが必要だったり少し面倒な事になっています。

// エンコードのロジックもビットシフト等に置き換えることで、半古典的手法よりはるかに高速化すry
// 有志のプルリク待ってます。

新旧.syxの比較する

意図した通りに.syxファイルが変更されているか確認します。
編集前のsyx/A000.syxと編集後のsyx/A000_mod.syxを比較します。
バイナリエディタ(0xED)にそれぞれのファイル読み込ませて目diffでも良いですが
手っ取り早い比べるためにhexdumpの結果をdiffに食わせます。

$ diff -u <(hexdump -Cv A000.syx) <(hexdump -Cv A000_mod.syx)
--- /dev/fd/11  2014-06-06 05:13:56.000000000 +0900
+++ /dev/fd/12  2014-06-06 05:13:56.000000000 +0900
@@ -1,10 +1,10 @@
 00000000  f0 42 30 68 73 01 40 00  00 03 00 48 4f 47 45 48  |.B0hs.@....HOGEH|
-00000010  4f 47 00 45 48 4f 47 45  48 4f 00 47 45 00 00 00  |OG.EHOGEHO.GE...|
-00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
+00000010  4f 47 00 45 48 4f 47 45  48 4f 00 47 45 5f 4d 4f  |OG.EHOGEHO.GE_MO|
+00000020  44 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |D...............|
 00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
-00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
+00000060  00 00 00 00 00 00 00 00  00 00 10 00 00 00 00 0c  |................|
 00000070  10 02 00 00 40 00 00 00  00 00 00 00 00 00 00 00  |....@...........|
 00000080  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

Combinationの名前の方は良さそうですね。

Effect Typeは0だったのを140(=128以上の値)に変えたので
所謂7バイト1組のそれぞれの最上位bitを寄せ集めて出来た1バイトも変更されているはずです。

アドレス0x0000006aに存在する0x10が上記に該当します。
0x10 = 0b00010000なので後に続く5バイト目のMSBを1にしたものが Internal data 形式の値です。

ちょうどアドレス0x0000006fに存在する0x0c = 0b00001100はMSBを1にすると
0b10001100 = 10進数の140なのでこちらも良さそうです。

KRONOSへMIDIシステムエクスクルーシブでリストア

KRONOSからMIDIシステムエクスクルーシブでダンプの逆パターンです。
SysEx Librarianを起動してAdd...ボタンを押してsyx/A000_mod.syxを追加します。
ファイル一覧上に現れたらsyx/A000_mod.syxをダブルクリックしてKRONOSに送信します。

s4.png

ただしこの時点ではKRONOSに音色データが転送されるもののKRONOSのメモリにライトされてない状態なので
CombiUAStoreBankRequest.syxもダブルクリックして送信します。
これの内容は

f0 42 30 68 76 01 40 f7

だけのメッセージでKRONOSが受信するとCombinationのU-Aバンクをメモリにライトしてくれます。

KRONOSの画面で確認すると

Before
k2.jpg

だったのが

After
k3.jpg
k4.jpg

に変更されています。以上で完了です。

あとがき

今回はCombinationの名前とEffect Typeを変えるだけだったので一見KRONOS本体でやってしまったほうが
早いと思われますが、これが100パッチ、200パッチ一括で命名規則変えたいなーとかエフェクターの値を
一括で修正するとか(他にも今回紹介していない弄れるパラメータは沢山あります)
そういった'バッチ処理的'な事をおこないたいときにPerlによるプログラムの威力を発揮します。

あとは同一メーカの旧機種の音色をいまどきのシンセにコンバートしたり、
他メーカの製品の音色データ(音色の系統やスプリット、レイヤーの設定くらいなら)ならメーカ間の壁を超えて
移植することも出来ると思います。(メーカ同士の音色を紐付けるmapを書かないといけませんがw)
// そんな時のためにGM縛りで音作りしておくとかry

シンセサイザを持っている方は説明書の最後のページにMIDI implementation chartが載ってるので
是非眺めて見てください。普段はあまり見ないかもしれませんがここから新たな発見があるかもしれません。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9