jackdaw開発日記(1) C言語でwavファイルを作る

jackdawというプロジェクトを開発しているのですが、後で自分の書いたコードを見返したときに分からなくならないように、いくつか書き残しておこうと思います。

今回はまずC言語でwavファイルを作る方法について書きます。

出力先ファイルを作る

まず空のファイルを作ります。open関数を使います。

#include <fcntl.h>

int open(const char *path, int oflag, ...);

第1引数 path にはファイル名を指定します。新しくファイルを作り、かつこの後プログラム内でファイルに対する読み込みと書き込みを行うので、第2引数 oflag には O_CREAT | O_RDWR を指定します(同様にファイルを作るcreat関数がありますが、O_RDWR の代わりに O_WRONLY が指定されてしまうため使えません)。第2引数 oflag が O_CREAT を含むとき、第3引数でファイルのアクセス権限を指定することができます。後でwavファイルを再生したり削除したりできるように S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH としましょう。返り値のファイルディスクリプタはこの後使うので変数に格納しておきます。エラーの場合 -1 が返ります。

ファイルを閉じるときにはclose関数を使います。

#include <unistd.h>

int close(int fildes);

第1引数 fildes にはファイルディスクリプタを渡します。

ここまでをまとめると以下のようになります。

#include <fcntl.h>
#include <unistd.h>

int main(void){
    char *out_filename = "a.wav"; //ファイル名は今のところこうしておきます
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;
    close(out);
}

空のファイルが作られます。ls -lで-rw-r--r--となっていることを確認して下さい。

マップする

次に、作ったファイルに対して書き込みを行う準備をします。このためには事前にファイルの大きさを知っておく必要があります。単位はバイトです。ビット深度/8 × チャンネル数 × サンプリング周波数 × 曲全体の秒数 + ヘッダサイズで計算できます。ヘッダサイズはこの後44バイトになります。

int samplerate;
short bitdepth, channel;
double length;
int filesize = bitdepth / 8 * channel * samplerate * length + 44;

現在ファイルは空であるため、中身を表すための領域がディスク上に確保されていません。まずこれを確保する必要があります。ftruncate関数を使います。

#include <unistd.h>

int ftruncate(int fildes, off_t length);

第1引数 fildes にファイルディスクリプタ、第2引数 length にファイルサイズを渡します。成功すると0、失敗すると-1が返ります。

次に、データを書き込むためにファイルをマップします。mmap関数を使います。

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags, int fileds, off_t off);

第1引数 addr にはNULL、第2引数 len にはファイルサイズ、第3引数 prot には PROT_WRITE、第4引数 flags にはMAP_SHARED、第5引数 fildesにはファイルディスクリプタ、第6引数には 0 を渡します。返り値はマップした領域のアドレスです。何らかのポインタ型変数に格納します。今回はビット深度に合わせてshort *型にします。エラーの場合 MAP_FAILED が返ります。

ここまでをまとめると以下のようになります。サンプリング周波数は44100、ビット深度は16、チャンネルは2(ステレオ)とし、秒数は今のところ3秒にしておきます。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    /* ここでデータを書き込む */

    close(out);
    munmap(map, filesize);
}

これで、map の参照先に代入した値がそのまま出力ファイルに書き込まれるようになります。

ヘッダーを書き込む

ヘッダーの中身はググると出てきます(私はwav ファイルフォーマットを見ました)
初めの4バイトは文字列"RIFF"
次の4バイトはファイルサイズ(filesize - 8)
次の4バイトは文字列"WAVE"
次の4バイトは文字列"fmt "
次の4バイトはヘッダーサイズ(16)
次の2バイトは波形フォーマット(1でいいらしい)
次の2バイトはチャンネル数(channel)
次の4バイトはサンプリング周波数(samplerate)
次の4バイトはデータ速度(samplerate * bitdepth / 8 * channel)
次の2バイトはブロックサイズ(bitdepth / 8 * channel)
次の2バイトはビット深度(bitdepth)
次の4バイトは文字列"data"
次の4バイトはデータサイズ(filesize - 44)
文字列の書き込みにはstring.hのmemcpy関数を使います。

memcpy(map, "RIFF", 4);
((unsigned int *)map)[1] = filesize - 8;
memcpy(map + 4, "WAVEfmt ", 8);
((unsigned int *)map)[4] = 16;
((unsigned short *)map)[10] = 1;
((unsigned short *)map)[11] = channel;
((unsigned int *)map)[6] = samplerate;
((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
((unsigned short *)map)[16] = channel * bitdepth / 8;
((unsigned short *)map)[17] = bitdepth;
memcpy(map + 18, "data", 4);
((unsigned int *)map)[10] = filesize - 44;

中身を書き込む

ここでは例として、440Hzの正弦波を書き込むことにします。math.hのsin関数を使います。コンパイル時に-lmを付けるのを忘れないで下さい。

for(int i = 0; i < samplerate * length; ++i)
    map[i * 2 + 22] =
    map[i * 2 + 23] =
        sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

プログラム全体は以下のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <math.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    memcpy(map, "RIFF", 4);
    ((unsigned int *)map)[1] = filesize - 8;
    memcpy(map + 4, "WAVEfmt ", 8);
    ((unsigned int *)map)[4] = 16;
    ((unsigned short *)map)[10] = 1;
    ((unsigned short *)map)[11] = channel;
    ((unsigned int *)map)[6] = samplerate;
    ((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
    ((unsigned short *)map)[16] = channel * bitdepth / 8;
    ((unsigned short *)map)[17] = bitdepth;
    memcpy(map + 18, "data", 4);
    ((unsigned int *)map)[10] = filesize - 44;

    for(int i = 0; i < samplerate * length; ++i) map[i * 2 + 22] = map[i * 2 + 23] = sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

    close(out);
    munmap(map, filesize);
}

実行すると440Hzの正弦波が3秒間流れるwavファイルが作られます。

今回はここまでです。
この記事があなたの役に立てば幸いです。

関数を作る

関数を実行時に作る

今回は関数を作ってみましょう。作るといっても、コンパイル時に作るのではありません。実行時に作ります。

全体の流れとしては、「①関数を置く領域を作る」→「②そこに処理を書き込む」→「③呼び出す」となります。今回は②のところで「4バイト整数を受け取り、これに100を足した数を返す」とすることにしましょう。

①関数を置く領域を作る

変数の型には、「int」や「double」や「char *」などの「1つの値を保管するもの」と、「int[]」や「char[]」や構造体などの「複数の値を保管するもの」があります。関数のこなす処理はそのどちらで表されるしょうか。もちろん、複雑な処理が4バイトや8バイトで表されるわけがありませんから、後者になります。関数はたくさんの小さな処理の積み重ねでできていて、1つ1つの処理は1〜7バイト程度で表されます。関数を作るときには、これらを連続したデータ領域に書き込む必要があります。

ここで注意しなければいけないことがあります。普段連続したデータ領域を作るときには配列を作ったりmalloc/calloc関数を使ったりしますが、関数の処理を書き込む領域をこのやり方で確保しても、呼び出して実行することができません(※環境による)。ハッキング防止のためにOSが実行禁止にしているからです。

そこで、別のやり方でデータ領域を確保する必要があります。mmap関数というものを使います。

mmap関数は、malloc関数を使ってある程度の大きさのデータ領域を要求したときに内部的に呼び出される関数です。簡単に言うと、動的にデータ領域を確保する手段の1つということです。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);

mmap関数の第3引数protで、そのデータ領域に対するどんなアクセスの仕方を許可するか指定できます。PROT_READは読み出しの許可、PROT_WRITEは書き込みの許可、PROT_EXECは実行の許可です。mallocを使うとこのPROT_EXECが指定されないため、実行できないのです。PROT_READ、PROT_WRITE、PROT_EXECのうち複数を指定したいときは | でビット和をとります。何も指定しないときはPROT_NONEとします(データにアクセスできなくなります)。

さて、今回は「関数の処理を書き込む」と「実行する」の両方が必要ですから第3引数protには「PROT_WRITE | PROT_EXEC」を指定します。

また、第4引数では実際にどの場所にメモリ領域を確保するのか指定します。例えばここで"hoge"という名前のファイルを作ってそのディスクリプタを渡すと、書き込んだ内容がhogeに出力されるのですが、今回は関数を作って使うだけでその中身をファイルに残しておく必要が無いので /dev/zero を使います。open("/dev/zero", O_RDONLY);の返り値を渡せばよいです。
2018/12/04追記:O_RDWRとしていましたがO_RDONLYで良いようです。

第1引数addrはNULL。第2引数lenは確保するバイト数。第4引数flagsはMAP_PRIVATE。第6引数offは0とします。

mmap関数は確保した領域の先頭アドレスを返すので、それをchar *型の変数に入れます。これで関数を置く領域が作れました。

また、malloc関数で確保した領域をfree関数で解放するのと同じように、mmap関数で確保した領域は使い終えた後にmunmap関数という関数を使って解放します。munmap関数の第1引数には確保する領域のアドレス、第2引数には確保していたバイト数を渡します。

ここまでの流れをまとめて書くと次のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    int fd, mapsize;
    char *map;
    fd = open("/dev/zero", O_RDONLY);
    mapsize = 100;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    close(fd);
    munmap(map, mapsize);
    return 0;
}

確保する領域のサイズmapsizeは一時的に100としておきましたが、作る関数に合わせて後で変更します。

②処理を書き込む

確保した領域に実際に処理を書き込んで関数を作ります。

今回は4バイトの引数を1つ受け取り100を足して返す関数なので、「push %rbp」(55)「mov %rsp,%rbp」(48 89 e5)「mov %edi,-0x4(%rbp)」(89 7d fc)「mov -0x4(%rbp),%eax」(8b 45 fc)「add $0x64,%eax」(83 c0 64)「pop %rbp」(5d)「retq」(c3)となりますね。これをconst charの配列として用意しておいて、memcpy関数を使ってさっき作った領域にコピーします(memcpy関数を使うにはstring.hをインクルードします)。

const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
memcpy(map, data, sizeof data);

これで関数が作れました!!!

③呼び出す

mapはchar *型なので、そのままでは関数として呼び出すことができません。関数ポインタ型という型にキャストする必要があります。

fncという名前の関数ポインタ型の変数を宣言するときは次のようにします。

int型の引数を3つ受け取ってint型の値を返す関数へのポインタ:

int (*fnc)(int, int, int);

引数を受け取らずdouble *型の値を返す関数へのポインタ:

double *(*fnc)(void);

第1引数がchar[]型、第2引数がint型で、値を返さない関数へのポインタ:

void (*fnc)(char [], int);

今回はint型の引数を1つ受け取ってint型の値を返すので、int(*fnc)(int)となります。mapをキャストしてこれに代入します。

int (*fnc)(int);
fnc = (int(*)(int))map;

そのまま初期化するなら

int (*fnc)(int) = (int(*)(int))map;

のようになります。

(※mapを初めからvoid *型にしておけばキャスト演算子を書く必要は無くなります。またmapという変数を使わずに初めからfncだけを宣言しても構いません。今回は何をしているのか明確にするため書いています。)

さて、準備は整いました。いよいよ作った関数を呼び出します。

fnc(3);

試しに表示してみると、ちゃんと100が足されて返ってくるはずです。

printf("fnc(3) = %d\n", fnc(3));

出力:

fnc(3) = 103

ここまでをまとめると、コード全体は次のようになります:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
    int fd, mapsize;
    char *map;
    const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
    int (*fnc)(int);
    fd = open("/dev/zero", O_RDONLY);
    mapsize = sizeof data;
    map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data, sizeof data);
    fnc = (int(*)(int))map;
    printf("fnc(3) = %d\n", fnc(3));
    close(fd);
    munmap(map, mapsize);
    return 0;
}

ちょっと簡潔にしたバージョン:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>

int main(void){
	const char data[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0x83, 0xc0, 0x64, 0x5d, 0xc3};
	int fd = open("/dev/zero", O_RDONLY);
	int (*fnc)(int) = mmap(NULL, sizeof data, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
	memcpy(fnc, data, sizeof data);
	printf("fnc(3) = %d\n", fnc(3));
	close(fd);
	munmap(fnc, sizeof data);
	return 0;
}

応用編

これを応用すると、「関数を返す関数」を作ることもできます。実質関数のカリー化(?)

「unsigned型変数nを受け取り、「2つのdouble型変数a, bを受け取り、aにbをn回かけた数を返す関数」を返す関数」を繰返2乗法で作ります。関数の宣言は次のようになります。

double (*make_pow(unsigned))(double, double);

C言語ってこんなこともできるんですね。でも考えてみれば結局返してるのは単なるポインタか。

実装は次のようになります。本当は確保したメモリを解放するための関数も別途作るべきなのですが面倒なので省きます。

double (*make_pow(unsigned n))(double, double){
    int fd = open("/dev/zero", O_RDONLY);
    const char data1[] = {0x55, 0x48, 0x89, 0xe5, 0xf2, 0x0f, 0x11, 0x45, 0xf8, 0xf2, 0x0f, 0x11, 0x4d, 0xf0}, data2[] = {0xf2, 0x0f, 0x10, 0x45, 0, 0xf2, 0x0f, 0x59, 0x45, 0xf0, 0xf2, 0x0f, 0x11, 0x45, 0}, data3[] = {0xf2, 0x0f, 0x10, 0x45, 0xf8, 0x5d, 0xc3};
    int mapsize = 6;
    for(unsigned i = n; i; i >>= 1) mapsize += (i & 1 ? 30 : 15);
    char *map = mmap(NULL, mapsize, PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    memcpy(map, data1, sizeof data1);
    int cursor = sizeof data1;
    if(n & 1){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf8;
        cursor += sizeof data2;
    }
    n >>= 1;
    while(n){
        memcpy(map + cursor, data2, sizeof data2);
        map[cursor + 4] = map[cursor + 14] = 0xf0;
        cursor += sizeof data2;
        if(n & 1){
            memcpy(map + cursor, data2, sizeof data2);
            map[cursor + 4] = map[cursor + 14] = 0xf8;
            cursor += sizeof data2;
        }
        n >>= 1;
    }
    memcpy(map + cursor, data3, sizeof data3);
    return (double(*)(double, double))map;
}

この関数に例えば11を渡すと、

double fnc(double a, double b){
    a *= b;
    b *= b;
    a *= b;
    b *= b;
    b *= b;
    a *= b;
    return a;
}

と同じ関数が返ります。実用性はそんなに無いんですがちょっと面白い。

この記事があなたの参考になれば幸いです。ではまた

jackdaw - 新たなDTMの可能性について

現在、電子音楽では主に12平均律が使われています。しかしそれ以外の5平均律純正律等の響きを利用したいこともあります。そんなとき普通のDAWでは12平均律以外の音がなかなか出せずに苦労します。特に1オクターブ中に13個以上の音を入れたいときには策がほとんど無いのが現状です。

そこでこの度新たなDTMの可能性を探るための試みとして、jackdawという、音階の概念の無いDAWを開発することにしました。

jackdawは純正律に特化しており、トニックという音を基準として全ての音の高さが整数比で扱われます。例えばトニックをドの音にしておくと、<3/2>はソの音、<2/1>は1オクターブ高いドの音を表します。このようにして全ての音を比のみで表すことで、自由に音階を作ることができます。

ダウンロード・インストール

現在ソースコードはここにあります。

github.comダウンロードしてmakeして下さい(make jackdaw)。

./jackdawに楽譜ファイル(書き方は後述)を渡して実行すると、その中身に即してwavファイルが作られます。出力ファイル名はオプション-oで指定しますが、指定されなければa.wavになります。

楽譜ファイルの書き方

exampleに例がいくつかあるので分からなかったらそれを見てください。

score文

「score{」で始まって「}」で終わる部分の中身が楽譜となります。

(tonic 440)

トニックを440Hzに設定します。

(modulate 3/2)

トニックを現在の3/2倍にします。転調に使います。

(tempo 120)

BPMを120にします。

(velocity 0.5)

全体の音量を0.5倍にします。

<3/2>:4

トニックの3/2倍の音程の、長さ4の音符です。

<3/2>0.5:4

トニックの3/2倍の音程で、音量が0.5倍になった、長さ4の音符です。

0:4

長さ4の休符です。

(rhythm 1,1,2)

リズムを(長さ1,長さ1,長さ2)の繰り返し(タタタン)に設定します。

音符の長さを省略するとリズムに従って長さが決まります。

音符をカンマ「,」で区切って並べるとメロディーが作れます。

<5/4>:1, <9/8>:.5, <1/1>:.5, <9/8>:1 (『七つの子』冒頭)

「/」を使うことで2つ以上のメロディーを重ねて同時に鳴らすことができます。

<5/4>:1, <9/8>:.5, <1/1>:.5, <9/8>:1 / <1/2>:2, <3/8>:1 (ベースラインを追加)

「|」を使うことで曲を区切ることができます。

 score文の中では#〜#で囲まれた部分と##から行末までがコメントとして扱われます。

set文

今のところ使い道は

set samplerate = 44100

くらいです。サンプルレートが設定できます。

他の書き方:

set {samplerate = 44100}

こう書いても同じです。こちらは途中で改行をはさむことができます。

define文

マクロを定義できます。定義したマクロはその後波括弧{}で括ることで使うことができます。

define melody = <1/1>:1,<3/2>:1,<2/1>:2

score{{melody},{melody},{melody}} ## メロディーが3回繰り返される

message文

標準出力にメッセージを出力できます。

message this is message.

最後に改行されます。

message {this is message.}

の方だと最後に改行されません。

comment文

何もしません。

import文

同じディレクトリ内にある他のファイルを読み込みます。

header.jkdhファイルに"message header is loaded."、main.jkdファイルに"import header"と書いておいて./jackdaw main.jkdを実行すると、"header is loaded."と出力されます。

tonicやtempo、define文などを書いたヘッダファイルを作っておくと便利です。

system文

C言語のsystem関数を呼び出します。"system rm -rf /"とすると"rm -rf /"が実行できます。

今後の課題

音源という概念が今の所存在しません。 (jackdaw.cを見ると分かるのですが今後音源をどう扱っていくかについては色々考えています)

あとはまだ色々バグとかあると思います、じっくり潰していきます

 

このプロジェクトがあなたの役に立てればこの上なく光栄です。

目の前に暖簾があったとき

目の前に暖簾があったとき、それを1万回押そうと試みる、私のような人間がいる。

暖簾を1回押す労力がどれほど軽いものであるかは皆が知っている。しかしそれが1万回になると誰も知らない。

私は、周りに誰もいないのを確認して、暖簾を押し始める。しかし1万回も続けることはできず、1000回押したところで疲れて諦めてしまう。しかしたとえ予定の1割であろうと、初めに思っていたよりは遥かに大変だったから、私はそれで十分とする。

私の労力が、例えば企業して1億円稼ぐことや勉強して司法試験に受かることと比べるとどれほどのものなのかなど誰も知らない。だから私は、自分の主観を存分に挟んでそれを評価することができる。

 

私は見えない努力をした。

 

しかし私は、外からの評価が無いと満たされない。そこで身近にいる人間を1人捕まえてきて、こっそり打ち明ける。「暖簾を1000回押した。これはお前の思うよりずっと大変なことだぞ」と。暖簾を押すために必要な仕事量に関するきちんとしたデータは与えず、なんとなくぼやかした言い方をする。

もちろんその人は暖簾を1000回はおろか3回も続けて押した経験が無い。その人が聞かされるのは、「暖簾を1000回押すことは、予想よりも大変なことだ」ということだけだ。

 

莫迦はそれを信じる。

 

かつて私は莫迦だった。そして今は、莫迦を捕まえる側にもなった。

私のような人間を、ある者は「哀れな世間知らず」と呼ぶかもしれないし、またある者は「こじらせた中二病」と呼ぶかもしれない。実際それで概ね正しいのだろう。しかしそういった声に対しては耳を塞ぐことができる。実際この世には十分に評価されない努力というものが存在するから、それを隠れ蓑にすることができる。

私は幸せなのだろうか。井の中を泳ぎ回るためだけに、私は生まれてきたのだろうか。本人が幸せだと思えば幸せなのだろうか。割り切ってしまえばよいのだろうか。もし世界中の人間がそうやって何もしなかったら、どうなってしまうのだろうか。

 

私は、なんとなくだが、この生き方が嫌いだと思った。

C言語で電卓を作ろう

C言語で簡単な数式を読んで計算するプログラムを作ります。

数式内で使える演算子は「+」「-」「*」の3つで、括弧は使えません。

使える数は0以上の整数で、負の数は使えません。

 

優先順位は「*」が高く、「+」と「-」が低くなるようにします。「2 + 3 * 4」は 20 ではなく 14 になります。

 

3つの変数 a、b、c を用意し、a と c を 0、b を 1 で初期化しておきます。

受け取った数式を最初から順番に読んでいって、「*」があったら b に a をかけ、「+」「-」があったら c に a * b を足します。これらの演算子を読んだ後、a に 0 を代入し、bに「+」なら 1、「-」なら -1 を代入します。数字を読み込んだ場合は a に 10 をかけて足します。

 

これを文字列の終わりまで繰り返したら、a * b + c が計算の答えです。

 

コードはこちら

github.com

 

+と-と*しか無いからこそ実現できるシンプルさです。

でも変数を1つ増やせば「/」も追加できないことはなさそう。

ただ、括弧を使いたいときにこのやり方は使えないので他のやり方を考える必要があります。

X

Arch LinuxGUI環境を構築した。GUI環境は欲しい時にだけ使いたくて、普段の作業はコンソールでやるつもりだったので、startxコマンドでXを起動→$mod+EでXを終了ができるようにした。ウィンドウマネージャはi3を選んだ。

 

xorg-server, xorg-apps, xorg-xinitをインストール。この時点でstartxとするとXが立ち上がり、即座に終了する。

 

i3, dmenu, xtermをインストール。さらにフォントを扱うfontconfig, xorg-font-utils も必要だが、これはXフォントを何か1つインストールすると手に入る(公式リポジトリにある otf-ipafont など)。

 

/etc/X11/xinit/xinitircを~にコピーして編集する。最後のtwmとかxclockとかxtermとかの行は向こうが勝手に走らせているだけなので消す(そもそもtwmとxclockはインストールしてない)。そしてexec i3と書き加える。

 

この状態でstartxとするとi3が起動する。言われるがままにmodキーなどを設定する。$mod+Enterでxtermを起動する。psでi3を探して殺し、コンソール環境に戻る。すると/.config/i3にconfigという設定ファイルが作られているので、これを編集して$mod+Shift+eのショートカットが書かれた行を bindsym $mod+Shift+e exit と書き換える。この状態でstartxとすると$mod+Shift+eで即座にXが閉じるようになっている。

 

キーボードレイアウトは/etc/X11/xorg.conf.dにファイルを作って指定する。00-keyboard.confの1つだけを書いた。

Section "InpurClass"
Identifier "system-keyboard"
MatchIsKeyboard "on"
Option "XKbLayout" "us"
Option "XKbVariant" "dvp"
EndSection

 

設定は完了した。firefoxをインストールして挙動を確認したりする。あと僕は/.config/i3/configの最後にbindsym $mod+Escape exec i3lock -c000000と書き加えて、即座に画面ロックができるようにしたりした。

Arch Linux インストールの流れ

BIOS/MBRUEFI/GPTのうち、より優れたUEFI/GPTを使う。

 

全体の流れ

1. インストール作業のための一時的な設定

2. パーティション分け

3. Linux自体のインストール

(chroot)

 4. 今後使う上で必要な設定

 5. 次回起動時のブートローダのインストールと設定

 6. 再起動

 7. 諸設定

 

Arch Linuxのインストールメディアは牛肉屋を営む牛のようなものである。牛が仕入れて客に提供するのは牛肉だが、牛が自らを捌いて売るわけではない。同様に、Arch Linuxのインストールメディアはインターネット上からArch Linuxをダウンロードしてコンピュータにインストールするが、インストールメディアの中のArch Linuxがコピーされるわけではない。また、ユーザーは初めのうちは売る方の牛に指示を出すが、途中からは売られた方の牛を調理するようになる。この切り替えがchrootである。

インストール作業の中でテキストエディタviが登場する。慣れないうちは操作が大変なのでnanoを代わりに使ってもよい。ただ今後のためにviを使うことを推奨する。

 

以上のことを押さえた上でインストール作業に取り掛かる。

 

インストールメディアの入ったUSBメモリーを刺し、有線でインターネットに繋ぎ、PCの電源ボタンを押して即座にF2、Boot List Optionで[UEFI]、Boot Option #1で[UEFI: (USBメモリの名前)]を選択、Save Changes and Reset。表示される選択肢の中からArch Linux archiso x86_64 UEFI USBを選択して起動、Welcome to Arch Linux! に続いてコンソールに文字が流れる。

 

tty1でroot自動ログインが行われ、入力を受け付ける状態になる。

archiso login: root (automatic login)

root@archiso ~ # _

 

1. インストール作業のための一時的な設定

Dvorakユーザなのでloadkeysを使って切り替える。

loadkeys dvorak-programmer.map

 

pingを使ってインターネット接続を一応確認する。

ping -c3 www.google.com

 2. パーティション分け

ディスク/dev/sdaのパーティションを決める。UEFIなのでgdiskまたはcgdiskを使う。僕はどちらかというと対話型のgdiskの方が使いやすいように感じた。/dev/sdaはコマンドライン引数と標準入力のどちらでも指定できる。

gdisk /dev/sda

コマンド入力を受け付けるようになる。

oで新しいパーティションテーブルを作成し、ここにパーティションを書いてゆく。

Command (? for help): o

This option deletes all partitions and creates a new protective MBR.

Proceed? (Y/N): Y

UEFIの場合まず/boot用のEFI System パーティション(ESP)を作る必要がある。Hex CodeとしてEF00を指定することでLinux filesystemではなくEFI systemとし、512M用意する。デフォルトのままのところは何も打たずにエンターを押せば良い。

Command (? for help): n

Partition number (1-128, default 1):

First sector () or {+-}size{KMGTP}:

Last sector () or {+-}size{KMGTP}: +512M

Current type is 'Linux filesystem'

Hex code or GUID (L to show codes, Enter = 8300): EF00

Changed type of partition to 'EFI System'

残りはすべて/用のrootパーティションとした。ここで/homeを分けてしまうと後でブートローダがエラーを吐く(指定の仕方を間違えたのだろうか)。

Command (? for help): n

Partition number (1-128, default 2):

First sector () or {+-}size{KMGTP}:

Last sector () or {+-}size{KMGTP}:

Current type is 'Linux filesystem'

Hex code or GUID (L to show codes, Enter = 8300):

Changed type of partition to 'Linux filesystem'

メモリが1GB以下だとswapも作ることが多いが、幸い4GB(十分!)もあったので作らなかった。swapを作るとしたらHex codeは8200になる。

そしてwriteで作成したパーティションテーブルをハードディスクに書き込む。

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING PARTITIONS!!

Do you want to proceed? (Y/N): Y

これで/dev/sda1(後の/boot)、/dev/sda2(後の/)の2つのパーティションが作られた。

 

ESPはF32でフォーマットする。

mkfs.vfat -F32 /dev/sda1

他はext4でフォーマットする。

mkfs.ext4 /dev/sda2

swapを作るならここでmkswap /dev/sdaXとswapon /dev/sdaXを行う。 

/dev/sda2(後の/)を/mnt自体にマウントする。

mount /dev/sda2 /mnt

 /dev/sda1(後の/boot)は/mnt/bootを作ってマウントする。

mkdir /mnt/boot

mount /dev/sda1 /mnt/boot

 3. Linux自体のインストール

viを起動してミラーリスト(どのサーバーから優先的に落とすか)を設定する。

vi /etc/pacman.d/mirrorlist

日本在住の場合Japanを検索して一番上に持ってくる。キーボード操作としては「/Japan(エンター)」で検索、「2dd{p」でコピー&ペースト、「n」と「N」で次を検索&前を検索、終わったら「:wq(エンター)」で保存&終了。

 

pacstrapでLinuxをインストールする。

pacstrap -i /mnt base base-devel

エンターを押し続け、言われたものを全てインストールする。

 

fstabを生成する。

genfstab /mnt >> /mnt/etc/fstab

次回起動時に/mntが/、/mnt/bootが/bootになる。ここでfstabが書き込まれた/mnt/etc/fstabを確認する(cat /mnt/etc/fstab)と、/dev/sda2がext4で/に、/dev/sda1がvfatで/bootになるという趣旨の表があるはずだ。genfstabに-Uオプションを付けていると左の列がUUID=(数字とアルファベットの羅列)になる。

 

肉屋で牛肉を買い終えた。このあとは肉屋を出て、受け取った牛を調理する。

arch-chroot /mnt

家に帰って牛肉を調理台に置いた。

 4. 今後使う上で必要な設定

viで/etc/locale.genを開く。

vi /etc/locale.gen

 「/en_US」などとして「n」「N」を使いながら「# en_US.UTF-8 UTF-8」と書かれた行を探し、「0x」でコメントアウト。同様に「/ja_JP」などとして「# en_US.UTF-8 UTF-8」を探し、「0x」でコメントアウト。「:wq」で保存終了。そして変更を反映。

locale-gen

 

locale.confで言語を設定する。

echo LANG=en_US.UTF-8 > /etc/locale.conf
export LANG=en_US.UTF-8

 

次回起動時のキーボードレイアウトとフォントを設定する。

vi /etc/vconsole.conf

 「i」で入力を開始、Escで入力を終了。

KEYMAP=dvorak-programmer.map
FONT=Lat2-Terminus16

とした。

 

場所の設定。

ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

時刻の設定。

 hwclock -u -w

ホスト名の設定。

echo (ホスト名) > /etc/hostname

 

インターネット接続にはdhcpcdを使う。systemctlコマンドを使い、起動時にdhcpcdが自動でインターネットに接続するようにする。systemctlの後に続けてオプションを指定する:その場で立ち上げるにはstart、その場で止めるにはstopを使い、enable↔disableは起動したときに立ち上がるかどうかを選ぶ。次回起動時に繋がらなかった場合はまたsystemctlを使ってdhcpcdの状態を確認する。

systemctl enable dhcpcd

 

パスワードを設定する。

passwd

 5. 次回起動時のブートローダのインストールと設定

Arch Linux自体の設定は完了した。現在Arch Linuxは起動した状態であり、ここからこのOSをずっと起動したままで使い続けることが可能である。いわば暖炉の中で薪が燃えている状態である。あなたはその火を使ってさまざまなことができる。

しかしあなたはまだマッチを持っていない。今燃えている火は、USBメモリのarchisoが点火してくれた火である。暖炉と薪は整ったが、マッチが無いことには火を付けることができない。

マッチとなるのはブートローダである。ブートローダには数種類あるが、今回はsystemd-boot(旧称gummiboot)を使う。インストール。

bootctl --path=/boot install

この後いくつかの設定を行い、USBを抜いて再起動する。設定に失敗すると次の再起動ができなくなる。その場合は慌てずUSBを挿し直して起動し、/dev/sda1と/dev/sda2のマウントを上と同じように行い、ここからやり直す。

 設定するのは/boot/loader/entriesディレクトリと/boot/loader/loader.confファイルの2つである。

まず/boot/loader/entriesに移動する。

cd /boot/loader/entries

/boot/loader/entriesには起動モードの選択肢1つごとに1つのファイルを拡張子confで作る。arch.conf(普段)とarch-fallback.conf(Fallback、何かあったとき用)の2つを用意する。

ルートパーティション/dev/sda2の固有PARTUUIDを調べる。

blkid -s PARTUUID -o value /dev/sda2

arch.confを編集して次のようにする。

title Arch Linux
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=PARTUUID=(PARTUUID) rw

PARTUUIDのところにblkidで調べたPARTUUIDを書く。長い文字列なので、メモするのが面倒ならblkidの最後に> arch.confと付け加えておくとPARTUUIDが表示される代わりにarch.confに出力される。

arch-fallback.confは

title Arch Linux Fallback
linux /vmlinuz-linux
initrd /initramfs-linux-fallback.img
options root=PARTUUID=(PARTUUID) rw

次に、既に存在する/boot/loader/loader.confを編集する。

cd ..

vi loader.conf

このファイルに一行ずつ設定を書いていく。必ず書かなければならないのは「editor no」の一行である。これを書かないと、「スペースキーを押しながら起動→eを押してinit=/bin/bashを書き加えた状態でArch Linuxを起動→パスワード無しでroot権限が手に入る!ガハハ!」なのでまずい*1。何も書かないとデフォルトでeditor yesになってしまう。

僕は、電源ボタンを押したら即座にArch Linux(fallbackでない方)が選択されて起動するようにした。そのためには「タイムアウト:0」と「デフォルト:Arch Linux」の2つを指定する。タイムアウトの方は何も書かなくても自動で0になる。書くとしたら「timeout 0」。デフォルトのローダは「default (ファイル名)」と書いて指定する。ファイル名のところにはさっき作ったファイル名arch.confの拡張子を除いた部分(つまりarch)を書く。「i」で入力開始、Escで入力終了。

最終的にloader.confの中身は次のようになる:

editor no
default arch

:wq」で保存終了。他の設定例:

editor no
timeout 3
default arch

起動してから3秒間選択肢が表示され、その間に何もしないとArch Linuxが起動する。ここで表示される選択肢をtimeout 0でも表示するには起動時にスペースキーを押す。

6. 再起動

exitしてUSBを抜いて再起動。

exit

reboot

起動したら

login: root

password: (パスワード)

でログイン。

  7. 諸設定

ユーザを追加する。

useradd -m -G wheel (ユーザ名)

作ったユーザのパスワードを設定する。

passwd (ユーザ名)

sudoersファイルを設定する。

visudo

# %wheel ALL=(ALL) ALL と書かれた行を探し、コメントアウト

さらに一度入力したパスワードが一定時間有効なままなのが嫌なのでDefaults timestamp_timeout=0を追記した。別に構わないならしなくて良い。

そうしたら今作ったユーザでログインし直す。

exit

*1:やってみた。Welcome to Arch Linux! の代わりに[root@archlinux /]# と表示され、パスワードが変更できた。