Discordの読み上げBotを作っている話

この記事は OUCC Advent Calendar 2022 の 19 日目の記事です。昨日の記事は yuyu さんの「StackEditで非プログラマーと協同開発した話」でした。

はじめまして、ciffelia です。今日は私が開発している Discord 読み上げ Bot「Koe」と Koe で採用している技術をご紹介させていただきます。

Koeのロゴ
https://github.com/ciffelia/koe

読み上げ Bot って何?

Discord のテキストチャンネルに送信されたメッセージをボイスチャンネルで読み上げてくれる Bot です。
聞き専の人たちが送信したメッセージを合成音声で代読し、会話に参加しやすくしてくれます。

Koeが動作する様子を写したスクリーンショット

特徴

  • VOICEVOX を使用した高品質な音声合成
    • ずんだもん、四国めたんを含む 10 人以上の音源から好みに合ったものを選択可能
  • 特定の語句の読み方を設定する辞書機能
    • 読み方が難しいユーザー名の人がいても安心
  • ネタバレ(||で囲むやつ)は読み上げをスキップ

読み上げの仕組み

ボイスチャットに参加した Koe は概ね以下の手順でテキストを読み上げています。

  1. Discord API からメッセージを受信
  2. Redis から辞書データを取得
  3. 読み上げるテキストを生成
  4. Redis から声の設定(プリセット ID)を取得
  5. VOICEVOX Engine にテキストとプリセット ID を送信
  6. VOICEVOX Engine から合成音声の wav データを受信
  7. ffmpeg で音声データを変換
  8. 音声データを Opus にエンコードし、Discord に送信

使っている技術

プログラミング言語: Rust

Koe は Rust で書かれています。Rust は高速かつメモリ安全なプログラミング言語で、以下のような特徴があります。

  • 機械語にコンパイルされる
    • C や C++ と同じコンパイラ言語です。
    • Python や JavaScript のようなインタプリタ言語と比較して非常に高速に動作します。
    • ただしコンパイルは比較的遅いです。
  • GC が存在しない
    • 後述するように、変数の生存期間がコンパイル時に確定するためガベージコレクタが存在しません。
    • GC がメモリ管理を行っている Go と比較して高速に動作します。
    • Discord が Go から Rust に移行してパフォーマンスを改善した話が面白いです: Why Discord is switching from Go to Rust
  • 型システムがメモリ安全性を保証してくれる
    • 変数がコードのどの部分で使われているのかコンパイラがチェックします。
    • メモリ安全であることが保証されないとコンパイルが通りません。
    • Rust の学習が難しいと言われる所以はこのメモリ安全性チェックにあります。
    • use-after-free をはじめとする C/C++ で起こりがちなバグを未然に防ぐことができます。
    • 他の言語に類を見ない特徴的な機能です。
  • 標準のパッケージマネージャがある
    • Cargo と呼ばれるパッケージマネージャが同梱されています。
    • Cargo ではパッケージのことを crate と呼び、crates.io で検索できます。
    • Node.js の npm と同じような使い心地です。
    • npm ほど酷くはありませんが、Depenency Hell に陥ることもあります。
  • 高機能で書きやすい
    • 開発が活発なため、便利な機能が他の言語から多数取り入れられています。
    • 例えば、関数型言語のように、配列にmapfilterを適用できます。
    • JavaScript や C#のように、async / awaitで非同期処理を行うこともできます。
    • シンプルさを重視し限られた機能のみを実装する Go とは対照的です。どちらが良いか好みが分かれる部分かもしれません。

Discord API: serenity / songbird

Rust で Discord API を扱うための crate として最も有名なのが serenity です。基本的な API 呼び出し機能に加えてキャッシュ機構を備えており、Discord API の扱いにくい部分を隠蔽してくれます。
また、Discord のボイスチャットに関する API を扱うための crate が songbird です。ボイスチャンネルに Bot として接続し音声を流す機能を備えています。

音声合成: VOICEVOX Engine

読み上げソフトウェアとして有名な VOICEVOX から音声合成エンジンを切り出したものが VOICEVOX Engine です。
VOICEVOX Engine に対して HTTP リクエストでパラメータを送ると、合成された音声データを返してくれます。

データベース: Redis

Redis は key-value store として使えるデータベースです。いわゆる NoSQL データベースの一つです。
Koe では辞書やユーザーの声などの設定を保存するために使用しています。

コンテナ管理: Docker

Docker は言わずと知れたコンテナ管理ソフトウェアです。コンテナとは、超軽量な Linux の仮想マシンのようなものなのですが、簡潔かつ正確に語るのは難しいので詳細は割愛します。
Koe ではdocker-compose.ymlを配布しており、コマンド一つで簡単に Koe、Redis、VOICEVOX Engine のコンテナを起動・停止できるようにしています。

リリース自動化: GitHub Actions

GitHub Actions は、GitHub の管理する仮想マシン上で様々なイベントをトリガーとして任意の処理を実行できるサービスです。
Koe では以下のような処理を自動化しています。

  • リポジトリにコードがプッシュされたとき
    • コンパイルが通るかチェック
    • コンテナをビルドして GitHub Container Registry にプッシュ
  • 私がリリースボタンを押したとき
    • 新しいバージョンをリリースするコミットを作成
    • バージョンに対応する Git のタグを作成
    • バージョンに対応する Docker コンテナのタグを作成
    • リリースノートを作成

依存しているソフトウェアのアップデート自動化: Renovate

Koe は様々なソフトウェアに依存しています。先述した serenity, songbird, Redis に加えて、設定ファイルのデシリアライザとして Serde、HTTP クライアントとして reqwest などを利用しています。
それらの新しいバージョンがリリースされるたびに手動で更新を行うのは面倒なので、アップデートを自動化する Renovate を導入しました。Renovate は指定した時間に自動で Pull Request を作成し、ビルドが通るとマージしてくれます。

おわりに

Koe を使ってみたくなった方はドキュメントを読めば簡単に導入できます。ソースコードが公開されているため改造も容易です。ぜひお試しください。
また、今回ご紹介した技術の多くは Discord Bot の開発以外にも活用できるものです。これらの技術を活用し、効率的なソフトウェア開発に役立てていただければ幸いです。

StackEditで非プログラマーと協同開発した話

執筆者:yuyu

この記事は OUCC Advent Calendar 2022 の18日目の記事です。前回はみやじさんによる「ASP.NET Core を使ってREST APIを作ってみる」でした。

最近の良かったことはサッカーのワールドカップで日本が出た全試合をリアタイし、サッカーに少し詳しくなれたことです。

もともとは「オフサイドってなんやろ」「4-4-2とは?」というレベルの知識で、選手に関しても長友選手や AbemaTV で解説を務めた本田圭佑などの時代しか知りませんでした。解説や Twitter のつぶやきを理解するためにルールや戦略論(フォーメーションなど)を調べていきサッカーの奥深さに少し気づけたかなと思います。

さて、今回の記事では、今年の春に非プログラマーと協同で web ページの作成を担当し、リーダーとして動いていた経験をもとに、当時意識していたことを書きたいと思います。

目次

  1. はじめに:背景
  2. 問題点
  3. 解決策
  4. 導入:開発裏話
  5. 結果
  6. おわりに
  7. リンク

1. はじめに:背景

私は別の団体と兼部をしているのですが(以下、G隊)、G隊では春の学祭にてクイズを使った出し物をしようということになりました。教室の中で完結するのですが、スタンプラリーのように順番に数問のクイズを解いていく形式で、最初の頃は設問を印刷して来場者に自分のペースで順番に解いてもらおうという議論でした。しかし、「様々な種類のクイズを用意したい」「答えは別用紙で用意する必要がある」「感染予防のため同じ紙を何枚もすらなければならない」などの理由から紙媒体ではなく電子媒体を用い、例えばQRコードを読み取ってもらってリンク先の web ページでクイズを読めるようにしようということになりました。結果的に印刷費用を抑えられるという利点もありました。

2. 問題点

非常にスマートで費用もかからないので現代の価値観に合った素晴らしい企画に思われましたが、ひとつだけ問題がありました。それは web ページを作ることができる人材の不足でした。

当時は私一人が G 隊の web 開発を担当していました。クイズ作成は複数人のチームで分担して行っていましたが、それを web ページにする仕事はひとりでやるには業務量が多すぎました。ぜひ web ページ作成も分担したいところですが、G 隊には web プログラマーは私一人しかいなかったので HTML & CSS で対応することは困難でした。

3. 解決策

HTML & CSS が使えないので Markdown 記法を用いて書けるシステムを採用すべきだと考えました。

Markdown(マークダウン)は、文章の書き方です。デジタル文書を活用する方法として考案されました。特徴は、

・手軽に文章構造を明示できること

・簡単で、覚えやすいこと

・読み書きに特別なアプリを必要としないこと

・それでいて、対応アプリを使えば快適に読み書きできること

などです。Markdownはジョン・グルーバー(John Gruber)によって2004年に開発され、最初は Daring Fireball: Markdown で公開されました。その後、多くの開発者の手を経ながら発展してきました。

Markdown とは(日本 Markdown ユーザー会)

Markdown 記法であれば少なくとも HTML & CSS よりは非プログラマーにも簡単に web ページを作ることができます。

では、Markdown 記法を使うとして、どんなツールを使えばよいのでしょうか。条件をいくつか出しました。

  • 共有しやすい(Git Hub 以外の方法で)
  • html ファイルに変換できる(サーバーをレンタルしていなかったため、GithubPages を使う必要があった)
  • 無料のサービスである

結果、"StackEdit"なるサービスを発見しました。これはブラウザ上で Markdown 記法を使って web ページを作ることができるサービスで、アプリのダウンロードが不要だったこともありがたく感じました。

StackEdit の素晴らしい点は Google Drive と連携できる点でした。G 隊はファイルの共有などは Google Drive で行っていたので今まで通りに各自で作成した web ページを Google Drive で共有できるというのは魅力的でした。進捗管理にも大変役立ちました。

また、html ファイルにエクスポートすることもできました。G 隊はサーバーをレンタルしておらず、web ページを持つために私の GitHub アカウントで GithubPages を作成して公開していました。GithubPages には html ファイルと css ファイルしか使えないという先入観がありました( md ファイルでもアップロードできたのかもしれないがよくわからん)。

StackEdit のエディター画面

デザインは凝ったものにできませんが、学祭でしか使わないページになるだろうということで妥協することにしました。

こうして、学祭担当のチームのみんなには StackEdit を使ってクイズのページを作成してもらい、Google Drive に共有されたファイルを私が html ファイルに変換して GitHub のリポジトリにアップロードするという体制ができあがりました。

4. 導入:開発裏話

ここまでは企画運営の方針に関しての話でしたが、ここからはチームのみんなに環境構築をしてもらう段階となります。導入時に参照するドキュメントを作成し、Google Drive 上に共有しました。

導入用ドキュメント

意識したことは IT 用語(プログラミングの用語や web 開発の文脈の用語)を使わないよう徹底したことです。自分の知らない言葉で埋め尽くされたドキュメントは読む気にならないでしょう。例えば英語の文章において98%以上の語彙を理解して初めて適切な読解が可能になると言われています(Hu & Nation, 2000; Laufer, 2010; Schmitt, Jiang & Grabe, 2011)。知らない語彙が一定数あると読解精度が落ちるわけですが、個人的にはこの現象は外国語の文章に限らず専門的な内容の文章を読む場合でも変わらないと思っています。

ドキュメントの整備に加えて、質問しやすいような環境づくりも意識しました。定例会議では何か困ったことがないか尋ね、相談してもらえるようにしました。

準備が整ったら各自好きなタイミングでクイズを書き、Google Drive にアップロードしてもらいました。それを私がチェックするという流れです。

5. 結果

クイズ画面
クイズ画面

クイズ画面は上のような感じになりました。スッキリとした印象で、問題を解くうえではかなり見やすい画面になっているように思います。プログラミングを使わなくてもここまでできるのは素敵ですね。

学祭当日は、私は忙しかったので運営として参加することはできませんでしたが盛況だったと聞いています。貢献できたようで嬉しい限りです。

また、この StackEdit 導入をきっかけに G 隊内において web 開発を始めてみることへの心理的抵抗感が弱まり、チームでの web 開発につなげやすくなったことは思わぬ幸運でした。

6. おわりに

新しくプログラミングに興味を持ってもらうには、「なんだか難しそう」という食わず嫌いを払拭させ、自分にもできそうだと思ってもらうことが何よりも重要でした。これはプログラミングに限らず新たな技術を導入する時には常に心掛けるべきことでしょう。チームの抵抗感を和らげ、スムーズな導入を目指しましょう。

OUCC Advent Calendar 2022 、次回の担当は ciffelia さんです!お楽しみに!

7. リンク

StackEdit:https://stackedit.io/

ASP.NET Core を使ってREST APIを作ってみる

ASP.NET Core を使ってREST APIを作ってみる

執筆者:みやじ

これは OUCC Advent Calendar 2022 の17日目の記事です。
ここではTodoアプリのバックエンドを想定して簡単な REST APIを作ってみようと思います

目次

プロジェクトを作る

Visual Studioで ASP.NET Core Web API のプロジェクトテンプレートを選択して作成します。
実行するとhttps://localhost:<port>/swagger/index.htmlが自動的に開き最初からある WeatherForecast API が表示されます。ここで<port>ランダムに選択されたポート番号です。よく見るSwaggerページだと思います。Try it out で試しに実行することもできます。

サンプルデータ用のサービスを作成する

まず次のようなTodoItemクラスを作成します。

public class TodoItem
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public string? Description { get; set; }
    public DateTime DueDate { get; set; }
}

つぎにそれを扱うITodoItemContainerを作ります。実装は少し長くなったので下の方に書いておきます。単純にList<TodoItem>を用意してそれに足したり抜いたりしているだけです。

public interface ITodoItemContainer
{
    IEnumerable<TodoItem> GetTodoItems();
    TodoItem? AddTodoItem(TodoItem item);
    bool RemoveTodoItem(int id);
    bool UpdateTodoItem(TodoItem item);
}

ASP.NET Core は依存性注入(DI)を採用しているのでITodoItemContainerを API Controller で使うためにはDIコンテナに登録する必要があります。 Program.cs でbuilder.Build();が呼び出されている箇所より上でbuilder.Services.AddSingletonを呼び出せば登録することができます。
今回は次のコードをを追加しました。

builder.Services.AddSingleton<ITodoItemContainer, TodoItemContainer>();

API コントローラーの作成

API コントローラーはControllerBaseを継承して作ります。これを継承することで応答に便利な関数を利用できます。

[ApiController]
public class TodoItemsController : ControllerBase

ApiCotroller属性をつけると以下のようなAPIの動作を有効化できます。

  • 属性ルーティング要件
  • 自動的な HTTP 400 応答
  • バインディング ソース パラメーター推論
  • マルチパート/フォーム データ要求の推論
  • エラー状態コードに関する問題の詳細

以下のように API コントローラーにメソッドを定義することでアクションを設定できます。ApiController属性を付けているとHttpGet属性なしでもメソッド名にGET POST PUT DELETEが含まれていると自動的にHTTPメソッドとして登録されますが、Swaggerが認識できないのでHttpGet属性をつけるほうが良いです。

[HttpGet]
public ActionResult<TodoItem> GetTodo() { }

ルーティング

ルーティングはRoute属性を用いてコントローラーまたはアクションに設定できます。ASP.NET Core のルーティングは大文字小文字の区別がありません。api/todoでもApi/Todoでも同じものとして扱われます。
コントローラにRoute属性を使うとそのコントローラーでのデフォルトのルートが設定されます。[controller]はコントローラーの名前から自動的に Controller の部分を取り除いた文字列が入れられます。
アクション、つまりメソッドにRoute属性を使うとそのメソッドだけにルーティングを設定できます。このとき、/または~/で始めるとコントローラーのルートテンプレートと結合されません。

また HttpGet などの HTTP 動詞テンプレートでもルーティングは設定できます。 HTTP 動詞テンプレートを使うとその HTTP 動詞だけを受け付けるように制限されるので REST API を作るなら全て HTTP 動詞テンプレートを使うこと方が良いです。 HTTP 動詞テンプレートは以下の6つです。

  • [HttpGet]
  • [HttpPost]
  • [HttpPut]
  • [HttpDelete]
  • [HttpHead]
  • [HttpPatch]
[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    [HttpGet]          // api/TodoItem
    public ActionResult<TodoItem> GetTodo() { } 

    [HttpGet]
    [Route("todo")]    // api/TodoItem/todo
    public ActionResult<TodoItem> GetTodo() { }

    [HttpGet("todo")]  // api/TodoItem/todo
    public ActionResult<TodoItem> GetTodo() { }

    [HttpGet("/todo")] // todo
    public ActionResult<TodoItem> GetTodo() { }
}

メソッドの引数

メソッドの引数はURLパスから受け取ったりリクエスト本文から受け取ったりできます。

URLパスから情報を受け取る

Route属性やHttpGet属性などで{id}のように書くことで、引数idにその値を受け取ることができます。また、{id?}とすることで省略することができます。省略された場合引数にはデフォルトの値が入ります。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    // api/TodoItem/1 => id = 1
    // api/TodoItem   => id = 0
    [HttpGet("{id?}")]
    public ActionResult<TodoItem> GetTodo(int id) { }
}

URLクエリパラメータやリクエスト本文から受け取る

それぞれ以下の属性を引数につけることでリクエストから受け取ることができます

  • FromQuery リクエストのクエリパラメーター
  • FromBody リクエスト本文
  • FromForm リクエスト本文内のフォームデータ
  • FromHeader リクエストヘッダー

FromBodyはSystem.Text.Jsonを使って自動的にパースしてくれます。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    [HttpPost]
    public ActionResult<TodoItem> CreateTodo([FromQuery] int id, [FromQuery] string title) { }

    [HttpPost]
    public ActionResult<TodoItem> CreateTodo([FromBody] TodoItem todo) { }
}

メソッドの返り値

メソッドの返り値にはActionResult<T>を指定しておけば暗黙的なT=>ActionResult<T>のキャストが実装されているのでとりあえずは困らにと思います。型が指定できないとき、例えば匿名型を使用するときなどはActionResultを使えばよいです。
そのまま値を返しても問題ありませんが、ControllerBaseに実装されているメソッドを利用することで一緒にStatusCodeを指定することもできます。よく使われそうなものをいかに列挙しておきます。

  • Ok(Object)
    HTTP 200 OK を返すオブジェクトを生成する。
    引数にはアクションの返り値にしたいオブジェクトを指定する。
  • Created(String, Object)
    HTTP 201 Created を返すオブジェクトを生成する。
    第1引数にはコンテンツが作成された URI を、第2引数にはアクションの返り値にしたいオブジェクトを指定する。
  • CreatedAtRoute(String, Object, Object)
    HTTP 201 Created を返すオブジェクトを生成する。
    第1引数には作成されたコンテンツを返すアクションの名前を、第2引数にはそのアクションのURLの生成に使用するルートデータ。第3引数にはアクションの返り値にしたいオブジェクトを指定する。
  • NotFound()
    HTTP 404 Not Found を返すオブジェクトを生成する。

もっと見る場合は ControllerBase クラスのドキュメントを参照してください。

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    private ITodoItemContainer _todoItemContainer;

    public TodoItemController(ITodoItemContainer todoItemContainer) {
        _todoItemContainer = todoItemContainer;
    }

    [HttpGet("{id}")]
    public ActionResult<TodoItem> GetTodo(int id) {
        var todo = _todoItemContainer.GetTodoItems().FirstOrDefault(t => t.Id == id);
        return todo is null ? NotFound() : Ok(todo);
    }

    [HttpPost]
    public ActionResult<TodoItem> AddTodo([FromBody] TodoItem item) {
        var todo = _todoItemContainer.AddTodoItem(item);
        return todo is null ? BadRequest() : CreatedAtAction(nameof(GetTodo), new { Id = todo.Id }, todo);
    }
}

完成

他にPUT DELETEメソッドを追加して最終的に次のようになりました。

TodoItemControllerの実装

[Route("api/[controller]")]
[ApiController]
public class TodoItemController : ControllerBase
{
    private ITodoItemContainer _todoItemContainer;

    public TodoItemController(ITodoItemContainer todoItemContainer) {
        _todoItemContainer = todoItemContainer;
    }

    [HttpGet]
    public ActionResult<TodoItem[]> GetTodoList() {
        return Ok(_todoItemContainer.GetTodoItems().ToArray());
    }

    [HttpGet("{id}")]
    public ActionResult<TodoItem> GetTodo(int id) {
        var todo = _todoItemContainer.GetTodoItems().FirstOrDefault(t => t.Id == id);
        return todo is null ? NotFound() : Ok(todo);
    }

    [HttpPost]
    public ActionResult<TodoItem> AddTodo([FromBody] TodoItem item) {
        var todo = _todoItemContainer.AddTodoItem(item);
        return todo is null ? BadRequest() : CreatedAtAction(nameof(GetTodo), new { Id = todo.Id }, todo);
    }

    [HttpPut]
    public ActionResult<TodoItem> UpdateTodo([FromBody] TodoItem item) {
        var result = _todoItemContainer.UpdateTodoItem(item);
        return result ? Ok(item) : BadRequest();
    }

    [HttpDelete("{id}")]
    public ActionResult DeleteTodo(int id) {
        var result = _todoItemContainer.RemoveTodoItem(id);
        return result ? NoContent() : BadRequest();
    }
}

ITodoItemContainerの実装

public class TodoItemContainer : ITodoItemContainer
{
    private List<TodoItem> _items;

    public TodoItemContainer() {
        _items = Enumerable.Range(1, 10)
            .Select(i => new TodoItem {
                Id = i,
                Title = $"todo item id:{i}",
                DueDate = DateTime.Today.AddDays(1)
            }).ToList();
    }

    public IEnumerable<TodoItem> GetTodoItems() => _items;

    public TodoItem? AddTodoItem(TodoItem item) {
        if (item.Id >= 1 && _items.Any(t => t.Id == item.Id))
            return null;

        if (item.Id < 1)
            item.Id = _items.Max(t => t.Id) + 1;
        _items.Add(item);
        return item;
    }

    public bool RemoveTodoItem(int id) {
        var todo = _items.FirstOrDefault(t => t.Id == id);
        return todo is not null && _items.Remove(todo);
    }

    public bool UpdateTodoItem(TodoItem item) {
        var todo = _items.FirstOrDefault(t => t.Id == item.Id);
        if (todo is null)
            return false;

        _items[_items.IndexOf(todo)] = item;
        return true;
    }
}

あとがき

HTTP Status Code がどれを使えばいいのか全然わからなかったです。加えてうまくSwaggerに反映されてくれなくてよくわからなかったです...

参考

dotnet-scriptを使ってC#でも書き捨てプログラムを

執筆者: しおかい

これは OUCC Advent Calendar 2022の12日目の記事です。
本題まで長いので読み飛ばしていただいて結構です。

動機

大学の講義というものは、工学部・基礎工学部に限らずプログラムを書かされることは間々あります。
そういったときに、特に言語を指定されなければ自分の慣れている言語で書きたいものです。

ところで、筆者にとってその「慣れている言語」というのがC#なわけですが、これがどうにも講義で使うのには相性が良くないのです(個人の感想です)。

講義でプログラムを求められる状況では、うちの場合大抵その一回限りの使い捨ての、コンソールアプリとすら言わないような実行して出力を見るだけといったようなものを書くことになります。
C++やPythonであれば、(予め環境構築が終わっていれば)適当に1つファイルを作って書き捨て、実行すればそれでいいので何も思うところはありません。

ところがC#(.NET)では、一番簡単なコンソールアプリでも、専用にフォルダを用意し、dotnet new consoleしてプロジェクトを作らねばなりません。
しかも、プログラムが記述されたファイルと実行ファイル以外にいろいろファイルやフォルダが生成される始末で、「書き捨て」をするには余りに仰々しすぎるのです。

試行錯誤

もっと、それこそC++やPythonのように、プログラムが記述されたファイルと、せいぜいコンパイル後の実行ファイル程度でどうかしたい。
そう思った私は、「そもそもdotnet経由で実行しようとするからプロジェクトが要るのでは」と思い、csc.exe(CSharp Compiler)を直接叩いてコンパイルしようと思い立ちました。
実際これでコンパイル、実行自体はできるのですが、いくつか問題がありました。

インテリセンスが効かない

まず、始めにやってみて気が付いたのは、単に.csのファイルを開くだけでは、VSCodeでインテリセンス等が全くと言っていいほど効かないのです!
C#と言えば静的解析(ほんとに?)。
それが効かないのは余りに面倒でした。

ではcscを叩く方法でC#にインテリセンスを効かすにはどうするか。
いくつか方法はあるのですが、最も単純なのは、*.csprojをワークスペースに配置することです。
これはdotnet new consoleすれば簡単に生成できます。
結局dotnet new consoleが必要なものの、1回で済むならだいぶ状況は良くなったと、初めはそう思いました。

フォルダ内にエントリポイントがあるファイルを複数置けない

この方法の問題点は何か。それは、VSCodeで開いたフォルダ全体が1つのプロジェクトとして認識されるため、エントリポイントを1つしか持てないということです。
言い換えれば、Mainのあるファイルがフォルダ内に複数あると、VSCode上でエラー表示が出るのです。

もちろん実際にはcscでコンパイルするのでエラーは無視できるのですが、うっとうしいですし、本当にエラーになっているものを見逃すかもしれません。
適当にファイルの中身の切り貼りを自動化すれば、一応大した手間なくエラーも無くコンパイルもできる状態にはできるものの、各.csファイルは単体でコンパイル/実行不可能なうえ、自動化用の諸々(当時私は専用のC#プログラムとtask.jsonで行っていました)がフォルダ内に散乱して、結局当初の目的をあまり達成できていませんでした。

光明

さてそうこうしいたある時、「そういえばC#をインタープリタ的に実行できるのがあった気がする」と思い出しました。
それについて調べたところ、dotnet-scriptというものを見つけ、これがまさに自分の求めていたものでした。
[注釈1]:ところで、先ほど「いくつか方法はある」といったところにC# scripts (CSX)と書いてあり、これがそのdotnet-scriptのファイルなのですが、ここに書いてあったことにこれを書いている今気が付きました。
(OUCC BLOGってMarkdonwのfootnote記法使えないのか……)

というわけで、今回このC# scriptsについての紹介になります。
(ここまで前置き)

本題

インストール

C# scripts(dotnet-script)とはどういうものなのかの話の前に、簡単にインストール方法を説明しておきます。
といっても、.NET6か.NET7のSDKをインストールすれば、あとはコマンドラインからdotnet tool install -g dotnet-scriptをするだけです。

できること

dotnet-scriptでは以下のことができるようです

  • アプリケーションへの埋め込み
  • Interactive実行(これが先ほど言った「インタープリタ」的実行)
  • スクリプト実行(主目的)

このうちアプリケーションへの組み込みは今回説明しません。

Interactive実行

コマンドラインでdotnet scriptまたはdotnet-scriptと入力することで、インタプリンタ環境が起動します。
文法は通常のC#とほぼ同じです。
リテラルや変数を;無しで打つと値を表示してくれます。
またおそらくImplicitUsing(後述)によるものですが、初めからある程度using済みなので、少し変わった電卓程度の用途には十分使えます。

なお起動後初めの出力が異様に遅いので、適当に数字を打って出力させておくといいでしょう。

一応後述のスクリプトファイルを読み込んだり、NuGet参照したもできますが、あまり複雑なことをするには(当然ながら)向いてはいません。
せいぜい打ち間違いに強い電卓でしょう(個人のry)。

なお終了は#exitで行えます。

スクリプト実行

さて本題、求めていたスクリプト実行です。
VSCodeの場合、ワークスペースでdotnet script initとすれば、直下にomnisharp.jsonmain.csxが、.vscode下にlaunch.jsonが生成されます。

これができればあとは好きにC#を書いていけます。
新規にスクリプトファイルを生成するのはdotnet-script new filenameでできます。
(普通にファイルを作っても問題ありませんが、たまにスクリプトファイルとして認識してくれずインテリセンス等が効かなくなることがあります。その場合、VSCodeを開きなおせば問題ありません。)

注意点として、Mainを書くと、実行時に
warning CS7022: プログラムのエントリ ポイントは、グローバル コードです。エントリ ポイント 'test.Main(string[])' を無視します。と言われて読み飛ばされてしまうので、トップレベルステートメントの記法を使用しましょう。
また、同じワークスペースで拡張子が.csのファイルを開いてしまうとインテリセンス等が効かなくなる場合があるので、拡張子を間違えないようにしましょう。
スクリプトファイルの拡張子は.csxです。
万一インテリセンスが効かなくなっても、.csなファイルを閉じてからVSCodeを開きなおせば問題ありません。

またImplicitUsingと思われる暗黙的にusingされている名前空間もあります。
一覧を見つけられなかったので、VSCodeのオートコンプリートを使って、aからzまで一文字打っては補完候補を見ていくという頭の悪い手法で調べたところ、dotnet-scriptで暗黙的にusingされているのはおそらく以下の通りです。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Text;
using System.Diagnostics;
using System.Dynamic;
using static System.Console;

コンソールアプリケーションとも少し違うようです。
System.Consoleusing staticされているおかげで、標準出力がWriteLine()で済むので楽です。

NuGet参照も使え、ファイル先頭に#r "nuget: PackageName, 1.0.0"のように書くことで参照できます。
ただし、これを追記した後はVSCodeを開きなおさないと、名前空間等をVSCodeが認識してくれないのが難点です。

また、実行はdotnet-script filepathで行えます。
VSCodeならdotnet script initしていればlounch.jsonが生成されているはずなのでF5でもいいでしょう。

一応コンパイル(発行)もdotnet-script publish filepathで行えます。
ただし何故かワークスペース直下にあるファイルをpublishできないので、コンパイルしたい場合は一段下の階層に入れましょう。

詳細な記法はGitHubを見るといいと思います(ほぼトップレベルステートメントと変わりませんが)。

トップレベルステートメントやImplicitUsingが分からない場合、アドベントカレンダー7日目の私の記事を見ていただければと思います(露骨な宣伝)。

終わりに

というわけで、dotnet-scriptによるスクリプト実行によって、C#で快適に書き捨てプログラムを書けるようになりました。
これで、「『ファイルを作ってハイ実行』って訳にはいかないから気軽にC#を使えない」と言ってC++やPythonを使っていた皆さんも、思う存分C#を書けますね!

pythonとArduinoと画像認識でタワーディフェンスゲームの自動化をしてみた

前書き

この記事はOUCC Advent Calendar 2022の8日の記事です。担当はMrMocchyです。
作成したプログラムはGithubに上げていますので全体をご覧になりたい方はこちらへどうぞ。
この記事ではゲームの自動化という内容を扱っています。これはゲーム開発者が想定していないだろう遊び方ですが、やっていることはプレイヤーの操作をプログラムに代替させているだけです。リソースを違法に爆増させるような類のチートではないのでお目こぼしください。
また、このゲームをやったことがないと何言ってるのか分からない部分も多いとは思いますが、そういうものだと思ってください。

自動化したゲームについて

自分はInfinitode 2というタワーディフェンスゲームを長いこと遊んでいるのですが、これはゲーム性として「プレイして得たリソースでタワーを強化し、次のプレイへ」という周回を繰り返す、盆栽ゲーと呼ばれることもあるタイプです。ステージごとにマップも敵の種類も大きく変わる、タワーと敵の相性により与えるダメージが0%~200%になる、無限の強化と無限の敵、etc...、とまあここはレビューサイトではないので切り上げますが、自分にとっては非常に奥の深いゲームだと思っているわけです。
しかしやりこんでいくとステージごとにある程度の最適解のようなものが見えてくるわけで、それを自動化に定評のあるらしいpythonを用いて放置ファームしようというわけです。

プログラム全体の流れ

上で書いた最適解、すなわちどこに何をするのか、というのをcsvに羅列してそれを順に行わせます。
csvファイルには[行動、座標、オプション](後ろ2つはあったりなかったり)を列挙しています。別の[行動、カテゴリ、ホットキー]を並べたhotkeys.csvから読み込んだ辞書型を参照して、[座標、カテゴリ、ホットキー]などを持つクラスのリストとして読み込みます。そして現在の所持金などからその行動をできるか判断して実行するというループでプレイさせます。
現在の所持金や設置・アップグレードのコストなどはPyOCRを使って画面から文字認識で読み取っています。

操作の自動化

Infinitode 2にはホットキー機能があり、キーボード入力一つでタワー設置やアップグレードからゲーム進行速度変更までできます。これをpyautoguiで操作しました。しかし、移動の矢印キーはホットキーで変更できず、pyautoguiが利きませんでした。調べた結果、自作キーボードなどに用いられるAtmega32U4というマイコンを積んだArduinoなら操作できるのではと考え、amazonでArduino Pro Microの互換機らしきものを購入しました。あとは

を参考に、pythonからArduinoにシリアル通信で文字を送り、それに応じたキーボード入力操作をさせました。

# control.py抜粋
import os
import serial
import time

#シリアル通信のセットアップ
ser = serial.Serial()
#デバイスマネージャでArduinoのポート確認
readlines = os.popen("powershell \"Get-CimInstance Win32_PnPEntity | Where-Object {$_ -like \'*Arduino*\'} | Select-Object Caption\"").readlines()
if readlines == []:
    print("error @ control")
    print("Arduino was not found")
    exit()
PORT = readlines[3][15:-2]#"COM5"の形のポートを取得
ser.port = PORT
ser.baudrate = 9600       #Arduinoと合わせる
ser.setDTR(False)         #DTRを常にLOWにしReset阻止
ser.open()
#終了時に ser.close() をする

def moveBy(dx,dy):
    string=b""
    if dx<0:
        string+=b"l"*(-dx)
    else:
        string+=b"r"*dx
    if dy<0:
        string+=b"d"*(-dy)
    else:
        string+=b"u"*dy
    ser.write(string)
    #ゲーム側のカーソル移動を待つ
    time.sleep(0.2*(abs(dx)+abs(dy)))
//move/move.ino
//Arduino側プログラム
#include "Keyboard.h"
void setup() {
  Serial.begin(9600);
  Keyboard.begin();
}
void loop() {
  if (Serial.available() > 0) {
    switch (Serial.read()) {
      case 'r':
        Keyboard.write(KEY_RIGHT_ARROW);
        break;
      //以下略
    }
  }
}

画像認識部分

などを参考に、ゲーム画面から現在の所持金やコストなどの文字の部分を切り取って文字認識で読み込みました。
ゲーム内で3:4のように表示された座標を読み取る際に特にうまく読み取れないことが多かったので、認識精度を上げるためにいろいろ試しました。その中で効果があったと思われるものを挙げます。

  • 切り取る画像の範囲を読み取りたい文字列いっぱいにまで狭める。
  • 黒字に白文字は誤認識しやすいようなので、で反転して二値化。(from PIL import Imageを使用)
  • 上の二値化ついでに画像を横に拡大して、細い半角の文字を細めの全角くらいにする。(数字の認識力は向上したが、コロンを誤認識することも増えたので結局使わなかった)
  • 切り取る部分によって二値化の閾値を調整
  • tesseract_layoutという読み取る方法(?)を変更する引数は、いろいろ試したけど結局6でよさげだった。

また、指定の画像を画面から見つけるのを用いて、設置コストの画面切り替えやポーズ画面の判定をしたりしています。

# ocr.py抜粋
def getImage(_left,_top,_right,_bottom):
    #ウィンドウが前面にあるか入力前にチェックし、なければカウントダウンして終了
    foregroundCheck()
    # ウィンドウサイズを取得し、ずれを調整
    f = ctypes.windll.dwmapi.DwmGetWindowAttribute
    rect = ctypes.wintypes.RECT()
    DWMWA_EXTENDED_FRAME_BOUNDS = 9
    f(ctypes.wintypes.HWND(hwnd),ctypes.wintypes.DWORD(DWMWA_EXTENDED_FRAME_BOUNDS),ctypes.byref(rect),ctypes.sizeof(rect))
    # 取得したサイズでスクリーンショットを撮る
    image = ImageGrab.grab((rect.left+2+_left, rect.top+31+_top, rect.left+2+_right, rect.top+31+_bottom))
    return image

def getTextFromImage(rect,border,show=False) -> str:
    img=getImage(rect[0],rect[1],rect[2],rect[3])
    if img == None: return
    img=img.convert("RGB")
    size=img.size
    img=img.resize((size[0]*2,size[1]*2))
    size=img.size
    img2=Image.new("RGB",(size[0],size[1]))
    #2値化して精度を上げる
    for x in range(size[0]):
        for y in range(size[1]):
            r,g,b=img.getpixel((x,y))
            if r+g+b > border*3:
                r,g,b=(0,0,0)
            else:
                r,g,b = (255,255,255)
            #img2.putpixel((x*2+1,y),(r,g,b))
            img2.putpixel((x,y),(r,g,b))
    txt = tool.image_to_string(img2,lang,builder=pyocr.builders.TextBuilder(tesseract_layout=6))
    #指定範囲の調整用に切り取った画像と二値化後の画像を並べて表示
    if __name__ == '__main__' or show:
        imgs=Image.new("RGB",(img.size[0]+img2.size[0],img.size[1]))
        imgs.paste(img,(0,0))
        imgs.paste(img2,(img.size[0],0))
        imgs.show()
    return txt

その他

csvファイルから読み込んだプレイ動作のアルゴリズムはAlgoという名のclassのリストで保存。
設置と同時のアビリティ取得や複数回アップグレードなどの特殊な命令は、複数のAlgoを続けてリストに追加している形です。

#csvdata.py抜粋
with open(f"csv/{stage}.csv","r") as file:
    reader = csv.reader(file)

    line = [row for row in reader]

    for lineNo in range(line.__len__()):
        l=line[lineNo]
        if l == []:
            continue
        if not hotkeys.dic.__contains__(l[0]):
            error(f"line {lineNo+1} : \"{l[0]}\" is not defined  (csv/{stage}.csv)")
        if l.__len__() > 2:
            #posありのとき
            pos=(int(l[1]),int(l[2]))
        elif l.__len__()==2:
            #posなしでoptionありのとき
            l.__add__(["",l[1]])
        if l.__len__() > 3:
            #オプションがある時
            if hotkeys.dic[l[0]][0]=="u":
                for i in range(int(l[3]) if hotkeys.dic[l[0]][0]=="u" else int(l[3])):
                    algos.append(Algo(line=lineNo,name=l[0],pos=pos,cate=hotkeys.dic[l[0]][0],hotkey=hotkeys.dic[l[0]][1:],curLoop=i,maxLoop=int(l[3])))
            elif hotkeys.dic[l[0]][0]=="t":
                algos.append(Algo(line=lineNo,name=l[0],pos=pos,cate=hotkeys.dic[l[0]][0],hotkey=hotkeys.dic[l[0]][1:]))
                algos.append(Algo(line=lineNo,name=f"ability{l[3]}",pos=pos,cate="a",hotkey=hotkeys.dic[f"ability{l[3]}"][1:]))
            elif hotkeys.dic[l[0]][0]=="a":
                algos.append(Algo(line=lineNo,name=f"ability{l[3]}",pos=pos,cate="a",hotkey=hotkeys.dic[f"ability{l[3]}"][1:]))
            continue
        algos.append(Algo(line=lineNo,name=l[0],pos=pos,cate=hotkeys.dic[l[0]][0],hotkey=hotkeys.dic[l[0]][1:]))

行動を実行する前にそれを可能か現在地、コスト、ゲームの状態(ポーズ画面やウィンドウがバックグラウンドにあるなど)を画面から読み取ってから判断します。
実行に成功したかを読み取って成功するまで繰り返すという方法も取れますが、あんまりスマートではないと思ってやめました。一応学習目的でもあるので。

#main.py抜粋
def canDo(a:csvdata.Algo):
    tile = ocr.getTileName()
    if a.cate == "g":
        return True
    elif ocr.isPause:
        return False
    elif tile == "":
        return False
    #現在のタイルに不適切な行動の判別と、コスト取得
    elif tile == "PLATFORM":
        if not(a.cate=="t" or a.cate=="m"):
            a.print()
            control.error("unable to do \""+a.name+"\" on "+tile)
        price = ocr.getPuttingPrice(a)
    elif tile == "SOURCE":
        if a.cate != "d":
            a.print()
            control.error("unable to do \""+a.name+"\" on "+tile)
        price = ocr.getPuttingPrice(a)
    elif tile=="ROAD" or tile =="BASE" or tile=="PORTAL" or tile=="MUSIC":
        a.print()
        control.error("unable to do \""+a.name+"\" on "+tile)
    elif hotkeys.dic[tile.lower()][0] == "t":
        if a.cate == "a":
            #アビリティはノーコスト
            return True
        elif a.cate != "u":
            a.print()
            control.error("unable to do \""+a.name+"\" on "+tile)
        price = ocr.getUpgradePrice()
    elif hotkeys.dic[tile.lower()][0] == "d":
        if a.cate != "u":
            a.print()
            control.error("unable to do \""+a.name+"\" on "+tile)
        price = ocr.getUpgradePrice()
    #コスト的な実行不可能の判断
    coin = ocr.getCoinNum()
    if price == None:
        return False
    if coin == None:
        return False
    if a.name == "bounty":
        return coin > price*(bountyNum +1)
    if coin-saving-price>0:
        return True
    return False

また、ウィンドウがアクティブでなくなったときはカウントダウンして終了、ポーズ画面になるとコンソールにcontinueと入力すると続ける、などのユーティリティ的機能をつけました。

# ocr.py抜粋
def foregroundCheck():
    def waitAndRecheck():
        time.sleep(1)
        if hwnd == win32gui.GetForegroundWindow():
            return True
        return False
    if hwnd != win32gui.GetForegroundWindow():
        if waitAndRecheck(): return
        print("exit in")
        if waitAndRecheck(): return
        print("3...")
        if waitAndRecheck(): return
        print("2..")
        if waitAndRecheck(): return
        print("1.")
        if waitAndRecheck(): return
        error("Infinitode2 window is not foreground")

isPause = False
def pauseCheck():
    global isPause
    if pyautogui.locateOnWindow(imagePass("endgame"),"Infinitode 2",grayscale=False,confidence=0.95):
        isPause = True

そして肝心のプレイのアルゴリズムのcsvですが、未完成です。最初の数分はプレイできているのでそのまま追加すれば大丈夫だろうとは思います。ファーム効率のいいステージはマップも広いのでもっと大変ですが。

プレイ画面

AutoInfinitode2プレイ画面

右下の操作ログ以外は単なるプレイ画面なんですが。地味ですね。
所持金(上部のコインのアイコン)の認識のため、全てのエフェクトを非表示にしているので映像でも地味です。

TODO

  • ステージのプレイアルゴリズムの完成
  • ゲームオーバー後に自動的にリスタートするループ
  • 変数とかクラスの名前、ファイルの整理
  • ファーム用ステージのプレイアルゴリズム作成

後書き

夏にBlenderのアドオン開発で初めて本格的に触ったpythonですが、やはり自動化にも手を出してみたいを思って始めました。特に普段の作業で自動化の需要がなかったのでゲームの自動化にしました。
切り抜く画像の座標合わせを始め、何かとゲーム画面を参照しながらやらなきゃならなかったので起動しっぱなしだったんですが、プレイしていないのにSteamでのプレイ時間が20時間くらい伸びてました。起動したまま飯食ってた時間も含めているので実際は不明ですが。

以上、pythonとArduinoと画像認識でタワーディフェンスゲームの自動化をしてみた、でした。
ご読了ありがとうございました。

音MADの話

執筆者: ちょくぽ

これは OUCC Advent Calendar 2022 の 12/9 の記事です。
記事中の〇はただの検索避けです

音MADをつくろう

音MADを作る動機はファンアートなどと同じですが、音MADに使われる技術は他のことにも使えます。基本的にDAWとvocalshifterの技術なので。

  • 合成音声に歌わせる・変な声を出させる
  • 歌の調整をする(人間でも合成音声でも)
  • 普通の動画で音ハメ

などなど……

前提

Q. 音MADって著作権的にどうなの?
A. 元ネタ(素材)の作者に訴えられた場合、確実に負けます。
音MADを作るなら、著作権者に目を付けられないよう、黙認されるよう常に心がけましょう。(要するに二次創作と同じ)

Q. 音MADって儲かるの?
A. 商用利用が許可されている素材だけで作れば広告を付けることはできますが、それが面白くなる確率はかなり低いです。そもそも儲けようと思わない方がいいです。著作権的にも。

Q. 消されないの?
A. 消されます。よっぽどでない限り動画削除だけで済ませてくれますが……

原作者や他のMAD製作者への、可能な限りのリスペクトor配慮をしましょう。
二次創作界の空気感としてもそういう所は特に大事です。というかどの界隈でも発信をするなら半年ROMれ。

海外のフェアユースの思考は日本の感覚とかけ離れているので参考にしてはダメです

音MADの技術の情報はどこで手に入る?

音MADでググるのも良いですが、ニ〇二コだと音楽+コメント付きなので情報が多いです(経験談)。素材はほとんど淫〇ですが。
というか、音MAD自体サムいネタで遊ぶのがメインのような界隈です。勘違いしないように

淫〇や2〇兄貴周りは面白い情報がちょくちょくあります
2〇式動画講座が有能すぎ
純粋な作曲に関する情報も、当然役に立つのでありです

素材集め

素材を集めます。

  1. 原曲
    MADの原曲となる曲を探して、必要な長さのmp3やmp4を入手します。ボカロなどは作者が配布しているケースがありますが、すべての音を手元で作ればかっこいい……(地獄)。

  2. 音声素材
    MADの楽器の音やボーカルにするために、素材を集めます。楽器にするならキャラクターの声質や音圧を重視し、環境音をドラムにしましょう。
    キャラクターに歌わせたいなら、一文字一文字あつめるのが一番早い。歌詞をガン無視して面白いセリフを音合わせする方が楽。

  3. 動画素材
    音MAD制作は音作りがすべてではありません。原作に寄せた動画素材を集めたり、MP4を透過したり……同じジャンルのMAD動画も参考にしましょう。手描きは自信があれば。

二次創作を許可しているコンテンツは探せばかなり多いです。
実際によく使われている素材は、BB配布素材のように権利者に削除する気がないもの、権利者がtwitterでMAD動画に言及しているものなど、セーフ寄りのグレーがほとんどですが……
(ニ〇二コで流行っている例のアレの多くは許可されているわけではないので注意)

ツール

  • REAPER
    DAWならなんでもいいですが、REAPERがおすすめです。有料として紹介されることが多いですが、普通に無料で使えます。

  • VocalShifter
    音声が一定の音程になるよう調整し、素材にできる。音の外しや調子など細かな調整もできる。

  • Wavetone
    テンポ測定や耳コピが必要なら。

  • 動画編集ソフト
    Aviutlなんてソフトは過去の産物なので、普通に最近出たソフトを使いましょう。私はAviutl使います。どれも機能はだいたい一緒です。

メイン

REAPERの操作は覚えれば便利な操作が書ききれないほどあるので、適宜調べてください
file

web上の適当なオンラインBPM計測などで音楽に合わせてクリックしてテンポを解析したら、REAPERに設定します。

原曲(写真上)をREAPERにドロップすれば、グリッド線がなんとなく波形に合います。

REAPER上ではマウスの他に Alt+ドラッグ、S、ctrl+shift+n、ctrl+D、F2、Alt+S などの操作をよく使います。適当に試して体で覚えましょう。

file

素材を母音などで分割し、音楽に合うように配置(大抵の場合、八分音符より細かい調整は必要ないです)
原曲の力を借りているので当然、これだけでMADとして聞けるレベルになります。実際、分割と配置のみでランキング上位になるような音MADも多いです。

エフェクト(ここではVST)でリバーブを付ける、パン(FXの左のつまみ)をトラックごとに左右にずらすなどすると聞きやすくなります

file

次に、音程を変えてみましょう
vocalshifterに素材を渡して素材をダブルクリックすると、ピッチ補正ができます。
黄色い線でピッチを平坦にすれば、音程を変えやすい素材として利用できます。黄色い線(今はD4)がこの音声の音程なので、覚えておきます。

file

これで作成したピッチ調整済wavファイルをREAPERに持っていきます。複数アイテムを選択してF2から一括でwavファイルを変更できます。

アイテムを選択してShift+0、Shift+9で音程を1ずつ変えられるので、MIDIファイルや楽譜、WaveToneなどとvocalshifterの黄色い線を見比べながら調整しましょう。単純作業です。
file

これができたら音作りはできたも同然です。
細かい技術は最初から調べるのではなく、とりあえず触ってから学んだ方が意欲も学習速度も上がります。

場所によって黄色の線を数音高くしたり低くすることでビブラートや感情をつけられます。
クオリティ高い音MADでは、次の手順を踏むものが割とあります。

  • vocalshifterで平坦にした声をREAPERで並べ、切り貼りタイミング合わせだけする
  • それを再度vocalshifterに戻し、音程をつける
  • 音程が大きく飛ぶところではしゃくりや裏返り、伸ばすところではビブラートなどをつける

スケールを外すと目立ちすぎるので、外すときの音は考えましょう。(これ以上は音楽理論の話)

Aviutlの編集は割愛しますが、BPMを指定してグリッド線を小説ごとにする設定項目があります。活用しましょう。
音が鳴るたびに素材を左右反転させる、ちょっとだけ大きくして跳ねさせる、などすると視覚的に楽しくなります。
あとイージングを親の仇のように多用しましょう。
モーショングラフィックス関連のプラグインも沢山あるので、Aviutlで何かを始めるときは即座にプラグインを検索しましょう。

おわりに

いかなる創作においても、数字を見るのはやめましょう。作るのが楽しいのです。
数字は作品のクオリティとは関係なく、題材と宣伝が全てです。マジで。

OUCC Advent Calendar 2022

次回のアドベントカレンダーは12/11、Ayaka氏です!

最近のC#のMainの書き方バリエーション

執筆者: しおかい

これは OUCC Advent Calendar 2022の7日目の記事です。
空いてたので即興で書きました。

最近の(と言っても.NET6とかまでの話ですが)C#にはいろいろ機能が追加されていまして、Hello Worldもいろいろな書き方が出来るようになっています。
dotnet new consoleして出来たProgram.csに

Console.WriteLine("Hello World");

の1行しかなく、にもかかわらず実行できるのを見て戸惑った人も居るのではないでしょうか。

という事で、今回はHello Worldを短く書くための3つの機能を紹介し、最終的に上記の1行コードにしていきます。
なお以下では特記無き限りMainは返り値void、引数string[]としました。

0.プレーンな書き方

最も基本的な書き方はおそらく以下のようになると思います。


using System;

namespace Plane
{
    public class Sample
    {
        public static void Main(string[] arg)
        {
            Console.WriteLine("Hello World");
        }
    }
}

SystemusingConsole.WriteLineでHello Worldしています。
特に言う事は無いでしょう。

1.ファイルスコープ名前空間

上記ではnamespaceでスコープを括っているが、ファイルスコープ名前空間という機能を使えばこの括りを外せ、インデントを1段減らすことが出来ます。

using System;

namespace Plane;
public class Sample
{
    public static void Main(string[] arg)
    {
        Console.WriteLine("Hello World");
    }
}

これは、ファイル全体が名前空間で括られているのと同じ扱いになります。

一応デメリットとして、ファイルスコープ名前空間を使うと1ファイルに1つしか名前空間を(通常の名前空間もファイルスコープ名前空間も)指定できませんが、普通1ファイルに複数名前空間を指定することは無いので問題ないでしょう。

2.ImplicitUsings

C#10からglobal usingという機能が追加されました。
これは例えば

global using System;

とすると、同じプロジェクトにある全てのファイルでusing Systemしているのと同じことになります。
.NET6ではこれを利用してImplicitUsingsという機能が提供されています。
これは読んで字のごとく「暗黙的なusing」であり、いくつかの名前空間を.NETが勝手にglobal usingしてくれるというものです。

コンソールアプリケーションでは

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

が自動的に行われた扱いになります。
内容はプロジェクトの種類によって異なる様です。
これを利用すると、Hello Worldは以下のようになります。

namespace Implicit;
public class Sample
{
    public static void Main(string[] arg)
    {
        Console.WriteLine("Hello World");
    }
}

using Systemが無くなりました。
幾分すっきりしましたね。

3.トップレベルステートメント

C#9から、トップレベルステートメントと言って、名前空間やクラスや関数の外、トップレベルの場所に直接式(ステートメント)を書けるようになりました。
すなわち、先ほどのHello Worldは、Mainの中身だけ抜き取って以下のように書けます。

Console.WriteLine("Hello World");

これが始めに出てきた1行コードの正体です。
実際にはコンパイル時にclassとMainが自動生成されている様です。
注意点として、トップレベルステートメントより上に名前空間やクラスを定義するとエラーになります。

// これはエラー
namespace TopLevel;
Console.WriteLine("Hello World");
// これもエラー
namespace TopLevel
{

}
Console.WriteLine("Hello World");
// これもエラー
namespace TopLevel
{
    Console.WriteLine("Hello World");
}
// これはセーフ
Console.WriteLine("Hello World");
namespace TopLevel
{

}

また、トップレベルステートメントを書けるのは1プロジェクトに1ファイルまでです。
なお、関数やusingは上に定義できます。

// これはセーフ
void Hello() => Console.WriteLine("Hello World");
Hello();

この関数はプロジェクト全体から見ることが出来ますが、アクセスは出来ないようになっています。

ちなみにこのトップレベルステートメント、awaitを使うと自動生成Mainは勝手にasyncになってくれます。
なので

Console.WriteLine("Hello");
await Task.Delay(5000);
Console.WriteLine("World.");

のようにTaskも使用できます。

ただし、refは直接使用できません。

var hoge = new[]{0, 1, 2};
// エラー
ref var e = ref hoge[^1];
e++;
Console.WriteLine(hoge[^1]);
// これはセーフ
void Hoge()
{
    var hoge = new[]{0, 1, 2};
    ref var e = ref hoge[^1];
    e++;
    Console.WriteLine(hoge[^1]);
}
Hoge();

終わりに

というわけで、C#でHello Worldを簡潔に書けるようになる機能3つを紹介しました。
決まり切ったことを省略できるのは便利で嬉しいことです。

大きく見た目の変わるトップレベルステートメントはともかく、ファイルスコープ名前空間とImplicitUsingsは便利なので使っていきたいと思います。

参考

Arduino Uno R3 で Japanino の水晶発振子を有効化する

執筆者: watamario15 (2022 年度部長)

これは OUCC Advent Calendar 2022 の 4 日目の記事です。結局誰も入れてくれなかったので、自分のストックをそのまま放出します。昨日までは、3 編に分けて OUCC の 3 年間と展望をお届けしておりました。そちらも興味があれば見ていってください。

(ここから本文)

昔入手した Japanino という付録マイコン、単体では何もできない (当たり前) ので放置していたが最近ふと調べてみると、水晶発振子が搭載されているのに使われていないという情報を発見した。そもそもこのマイコンを活用できていない以上何の意味もないわけだが、使えるはずのものが使われていないというのはどうにも気持ち悪いものだ。で、方法を調べてみると fuse ビットを書き換えると有効化できるようだが、それには書き込み装置か別の Arduino が必要らしい。ただ「使えるはずのものが使われていないというのはどうにも気持ち悪い」というだけで購入するようなものではない。ということで、結局諦めていたのだった...

話は変わるが、2022 年春夏学期、実験 B の授業が始まった。COVID-19 の感染状況が読めない中、いつオンラインにせざるを得なくなっても対応できるように毎回部品類を持ち帰るシステムとなった。また、今年からボードを Arduino (Uno R3) に切り替え、内容を一新したらしい。そして、毎回持ち帰る部品類には当然 Arduino も含まれている。

...ここで何か気付くだろうか。

fuse ビットを書き換えると有効化できるようだが、それには書き込み装置か別の Arduino が必要らしい。

あるじゃないか!!まさに今、手元に!!

ということで、[Japanino] 外部水晶発振子を使う: 案山子のメモ帳を参考に 2022 年にこの作業を実施してみたログを残したいと思う。

要件

  • Japanino (持ってなかったらそもそも読んでないと思うけど)
  • Arduino Uno R3 (他のボードは知らない)
  • PC (Uno R3 にスケッチを書き込み、シリアルモニタから操作する)
  • ジャンパワイヤ6本

なお、この手順に従ったことによって発生した損害に関して私は一切責任を負わない。自己責任で実施すること。

手順

  1. 普通に最新の IDE を https://www.arduino.cc/en/software から入手する。Japanino 用は Uno R3 に対応していないので使えない。
  2. https://playground.arduino.cc/Code/Programmer2/ の一番下にある programmer2.txt をダウンロードする。japanino-quartz.ino みたいなファイル名にして、その名前のフォルダに入れておくとそのままスケッチとして開ける。
    • もし削除されてダウンロードできなくなった等の際には、Internet Archive を使う。
  3. ダウンロードしたファイルをスケッチとして Arduino IDE で開き、以下のような編集を加える。
    • 240 行目ぐらいの謎の隙間に #define BYTE 0 と書く。別に void DebugLCD::clear() より前であって、#ifdef の中でなければどこに書いてもいいけど。
    • 450 行目付近の void DoSTK() 関数内の case 'a': 直後に break; を追記する (case 'a': break; でいい)。タイプミスで bootloader をふっ飛ばさないようにするための保険。
    • 1000 行目付近の void Fuse_Arduino_Style() 関数内の case atmega168: で、CMD_Write_Fuse_High( 0xdf);CMD_Write_Fuse_High( 0xdd);CMD_Write_Fuse_Low( 0xef);CMD_Write_Fuse_Low( 0xc6); に書き換える。
    • 1100 行目付近の loader8loader168 (大量の数値が並んでるところ) に const 修飾を加える。要するに unsigned char PROGMEM loader8[]const unsigned char PROGMEM loader8[] に、unsigned char PROGMEM loader168[]const unsigned char PROGMEM loader168[] にすればよい。
  4. Arduino Uno R3 に書き込む。
  5. 以下のように Arduino Uno R3 と Japanino をジャンパワイヤで結線する。Arduino Uno R3 を PC に接続していれば、この結線によって Japanino にも電源供給されるはず。
    Arduino Japanino
     +5V --- +5V
     GND --- GND
     D10 --- Reset
     D11 --- D11
     D12 --- D12
     D13 --- D13
  6. Arduino Uno R3 を PC につないだままの状態で、シリアルモニタを開いて 19200 baud に設定する。もし外してしまっても再接続すればよい。
  7. *s を送信してシグネチャを確かめる。Arduino IDE 2 では Ctrl + Enter が送信。
    • 何も表示されない場合は Arduino Uno R3 を外して Arduino IDE を閉じ、再度 Arduino IDE を立ち上げて Arduino Uno R3 を接続してシリアルモニタを開き、コマンドを再送信してみる。もしそれでもうまくいかなければ、Japanino 用 IDE 等の古いバージョンのシリアルモニタを開いて試してみる。
    • Arduino Uno R3 に書き込んだスケッチはずっと動いているので、IDE の再起動やバージョン変更に伴うスケッチの再コンパイルは不要。
  8. *A を送信する。Writing から始まる文字列が表示されるはず。
    • 小文字にすると全く違う意味となり、手順3で該当箇所の修正を行っていない場合は最悪 bootloader が吹っ飛ぶので注意。
  9. *u を送信し、以下の出力と一致すれば成功(ただし => 以降は補足のための追記)。
    Lock Bits: 11111111 => FF
    Fuse Low:  11000110 => C6
    Fuse High: 11011101 => DD
    Fuse Extn: 11111000 => F8

    逆に以下なら元のままで、変更できていない。以上の手順を再確認してほしい。

    Lock Bits: 11111111 => FF
    Fuse Low:  11010010 => D2
    Fuse High: 11011110 => DE
    Fuse Extn: 11111000 => F8

念のため、書き込んだときに自分が得た出力を掲載する。はじめ最新 IDE では *s*A が利かず Japanino の IDE で実行した画像となっているが、その後最新 IDE でも利くようになった。謎。

出力

あいにく私は精度を確認できる機器を持ち合わせていないため、本当にこれでうまくいっているのかどうかは分からない。実際に確認できた人がいれば、Twitter 等で教えてほしい。

おまけ - Japanino を 2022 年最新環境で使う

以上の作業では Japanino にスケッチを書き込む必要がないため、Japanino 用の環境構築は必要なかった。しかし、実際に Japanino 用に開発を行う際には環境構築が必要になる。以下にその手順を記す。

ドライバ

Japanino 用 IDE に付属するドライバは古いので、https://jp.silabs.com/developers/usb-to-uart-bridge-vcp-drivers の「ダウンロード」から最新のドライバを取得することを推奨する。Windows なら「CP210x Universal Windows Driver」、macOS なら「CP210x VCP Mac OSX Driver」だろう。

ちなみに、インストールは Windows に関しては .inf ファイルを右クリック (Windows 11 ならさらに「その他のオプションを表示」) で「インストール」を選択してもいいし、Japanino を接続すればデバイスマネージャーに警告表示になっているデバイスが現れるはずなのでそれを選択し、「ドライバの更新」からダウンロードしたドライバを選択してインストールしてもよい。前者は何も表示されないのでちょっと心配になるが、Japanino を接続して Arduino IDE が認識すればインストールできている (最新版 IDE なら不明なボード扱いになるが問題ない)。macOS は知らないので、自分で調べてほしい。多分普通のドライバインストールと同じ。

IDE

Japanino 用 IDE を使ってもいいのだが、Arduino IDE 2 が使えるようになった今あの太古の昔の IDE は使いたくないだろう。でも朗報だ。普通に接続して表示されたポートを選び、ボードとして「Arduino Pro or Pro Mini」を選択した上で「ツール -> Processor」から「ATmega168 (3.3V, 8 MHz)」を選べば普通にコンパイルと書き込みが行える。これは VSCode で行う場合も同じ。

OUCC の 3 年間を振り返る [後編]

執筆者: watamario15 (2022 年度部長)

これは OUCC Advent Calendar 2022 の 3 日目の記事です。昨日は中編をお届けしました。

2022 年度

2022 年度は、2021 年度に進めた改革の続き、2021 年度には部室の処理があったためにできなかった「コンピュータクラブ」としての活動の再興、そして後輩への継承を進めました。私は部長となりましたが、恐らく部の運営に関わる最後の年になるため、引継ぎを考えて関与量は 2021 年度よりはかなり減らしました。

新歓 (講習会、チーム開発など, 4-6 月)

2021 年度と同じ流れですが、部員数がかなり回復したことで、講習会担当者の割り振りはものすごくスムーズでした。結果、1 人で 3 つ担当するとかいう 2021 年度の私のような人は生じず、1 人 1 つ以下で回せました。COVID-19 もある程度落ち着いてきていたため、副部長が中心となり、大学の教室を借りて 3 年ぶりの対面講習会も実施されました。2021 年度に引き続き、RAINBOU による競プロ講習会 (@KowerKoint2010) も実施しました。また、チーム開発も全員参加ではなく、興味のある人の任意参加としました。2021 年度までは「自由な緩い部活」を謳いながら、人数の都合上強制参加のイベントがそれなりにあるという矛盾状態にあったのですが、2022 年度の人数なら任意参加でもできると判断しました。これによってか、あるいは流行りのクロスプラットフォーム開発環境 Flutter を選んだのが良かったのか、今回は全て少なくとも「使える」レベルの一定の完成度に達しました。

新歓イベント
対面講習会

各開発プロジェクトのリンクはこちらです:

ちなみに、こうして掲載できていることからわかるように、この開発は完全に OSS で行われました。このまま OSS で開発することが当たり前となり、公開コミュニティ化が推進されていくと嬉しいものです。

いちょう祭 (5 月)

部室を手放したことで財政的には安定していましたが、それは「存続できる」という意味であって、何か古い機器の更新など、設備投資ができる状態ではありません。入部障壁を下げるために部費を減額した以上、学祭は重要な収入源となります。この時期は新歓という意味でも重要です。という訳で、まちかね祭に引き続き久し振り (3 年ぶり) に開催されたいちょう祭にも出展することになりました。

さて、いちょう祭自体は 3 年ぶりですが、やることはまちかね祭と同じです。なので、特に問題なく...行きませんでした。まず、懲りずに食品模擬店と展示を両方出展したので、やはりカツカツでした。そして、一番は思ったより客足が無かったことと、唐揚げが他の 5 店舗と被ったことでした。被りについてはギリギリまで知らされなかったためどうしようもなかったのですが、結果大量に余り利益が出ませんでした。まあ、原価率 25% なので大赤字とはならず、15163 円の赤字で済みましたが。

食品模擬店

これで懲りたので、以降は「いちょう祭ではゲーム展示 (新歓、活動紹介) のみを行い、まちかね祭では食品模擬店 (活動資金の獲得) のみを行う」という方針になりました。

夏の開発 (8 月)

2 年前から続くアクション RPG の改良、2021 年度の会話ボットの強化、画像判別の新規制作が行われました。これらは 11 月のまちかね祭での出展を目指して制作され、実際に使用しました。2022 年度のまちかね祭に関しては、副部長が記事を作成しているそうなのでここでは省略します。ちなみに、まちかね祭では食品模擬店という方針にしたと先ほど書いたところですが、これについては、部員がもう食品は懲り懲りだということで、今年度限りでまちかね祭もゲーム展示とする方針になりました。

KC3 2022 (9 月)

毎年恒例 KC3 2022 です。今年度はハイブリッド形式となり、2 年ぶりに対面開催が復活しました。OUCC からは @kakikaki85 による「画像認識で遊んでみよう」と私のニッチな「電子辞書でプログラミング」を出しました。高い技術力を持った関西の情報系学生が一堂に集まったことで、刺激を受けた部員も多かったはずです。

まとめ

まちかね祭の話を省略するので、12/1 時点で書ける内容はこの辺りです。遺産の処理が終わったこと、部員数が回復したこと、財政的にも余裕ができたことで、部の運営は随分と安定するようになりました。後輩も積極的に運営を支えてくれており、来年度以降も期待できます。

今後の展望

私が目指していたものは、「同じ大学のコンピュータに興味のある人と情報交換ができたり、(主に)情報系学科なら履修や講義の相談ができたりする」コミュニティでした。これは、従来の「クラブ・サークル」としての在り方とは一線を画すものであり、同時に現代の多くのコミュニティの姿でもあります。特に Discord 等のオンラインコミュニティ形成に便利なサービスが登場したことによって可能になったものでしょう。今ではソフトウェアを GitHub で公開して Twitter で発表する、ということが誰でもできるようになったため、既に技術を持っている人は大抵自分でプロジェクトを決めて開発するようになり、クラブ・サークルに所属する流れは縮小に向かっているのだと思います。冒頭に述べたとおり、私も元はクラブ・サークルへの所属に消極的でした。では、それを邪魔することなく、意見交換などができるコミュニティとしての OUCC ならどうでしょうか。これなら、既に技術を持っている人でも入部、あるいはそこまでいかなくとも公開コミュニティへの参加までは検討してもらえるのではないかと期待しています。そして、そういうコミュニティは、時代が変わっても常に需要があるものだと思うんですよね。そういう意味で様々な改革を進めましたが、まだ不完全な部分は多いです。これをどういった方向に発展させていくのか、あるいはまた根底から覆して全く別の OUCC に改革するのか、これは次世代に託します。

様々な苦難を経て、ようやく安定した基盤を確立できました。余裕ができてこそイノベーションのきっかけが生まれるので、これは重要です。来年度以降、この基盤の上にどういった情報系コミュニティ OUCC が生まれるのか、是非とも期待したいところです。

おわりに

12/5 - 12/11 にかけて、Osaka University Giving Campaign 2022 なるイベントが開催されます。今回が初めてのイベントであり、我々としてもつい先日大学からメールがあったところなのでよく分からないところも多いですが、東大初のベンチャー企業 Alumnote が、スポンサー企業から集まった寄付金を得票数によって分配するイベントのようです。胡散臭さがすごいですが、主催は大阪大学となっており、大阪大学の内部ツール KOAN でも学生センターから通知が行われたため、怪しいイベントではないでしょう。既に述べたように、OUCC は資金面において安定して存在できる状態にはありますが、設備投資が行える状態にはありません。最新機器 (考えているものは Apple Silicon Mac や VR 機器) を導入することができれば、OUCC はコミュニティとしての価値のほかに、新しい技術を気軽に試し、学び、新たなイノベーションに繋げる遊び場としての価値も持つことができるようになります。本記事で紹介した OUCC のここ 3 年間の活動や、今後の展望に賛同いただける方は、「大阪大学コンピュータクラブ」にぜひご投票をお願いいたします。各日「正午から 30 分に最も多くの票を集めた団体」に追加で資金分配が行われる「デイリーチャレンジ」なるものがあるようなので、もし可能であればこの時間に投票いただけると有難いです。

最後に、OUCC の公開 Discord サーバーの招待リンクを掲載しておきます。荒らしやスパムでなければ、阪大関係者でなくとも誰でも参加可能としておりますので、他大学の方でも高校生以下の方でも、興味のある方は気軽にご参加ください。なお、スパム対策として「自己紹介」チャンネルで簡単な自己紹介を投稿した参加者にのみ、他のチャンネルに書き込めるようになるシステムとなっておりますので、その点はご協力をお願いします。

招待リンク: https://discord.gg/jBM2NP7ZxK

OUCC の 3 年間を振り返る [中編]

執筆者: watamario15 (2022 年度部長)

これは OUCC Advent Calendar 2022 の 2 日目の記事です。昨日は前編をお届けしました。明日は後編をお届けします。

2021 年度

2021 年度には、曖昧な数え方になっていた「部員」「OB」を明確化したり、それに併せて 10 年以上誰も触ってこなかった部の規約を実情に沿うように大幅に改正したり、部室を引っ越したり、既に経験者がいなくなっていた学祭に参加したり、活動形態を変更したり、顧問が交代したりと OUCC にとって大きな局面となりました。ちなみに、当時私は役職上は 2020 年度の流れで ANU CSSA との橋渡し役を担っており、副部長 (当部における実質リーダー) ではなかったのですが、なぜか部の方向性を大幅に担っていたり副部長より部を回していたりと不思議な立ち位置にいました。

新歓 (講習会、チーム開発など, 4-6 月)

2020 年度に新入部員 (我々世代) がそれなりに入ったため、人数的には 2020 年度よりマシでした。しかし、注意しなければならないのは「2 年目部員がほとんど」というところです。講習会できるネタを持っていない部員も多く、割り当てにはかなり苦労しました。結果、自分は C, C++, Python の 3 つの講習会を受け持つことになりました() なお、この年度から新たな取り組みとして、2020 年に大学公認団体に加わった阪大競技プログラミング部 RAINBOU から @small_onions さんを招き、競プロ講習会を実施しました。同じ大学の情報系団体として、交流を深めていきたいものです。

チーム開発では、パスワードマネージャ、会話ボットの新規開発と、2020 年度のアクション RPG、既存ゲーム「阪大大戦」の改善の 4 つのチームに分かれ、1 か月間の開発を行いました。「ハッカソン」の名は用いていないので、期間が長くとも問題はありません (キリッ。ちなみに、自分はパスワードマネージャでした。

Password Manager
会話ボット

なお、この頃は講習会のハイライトやイベントの告知など、頻繁に Twitter 投稿を行いました。その甲斐あってか新入生が 10 人ほど入部し、OUCC に活気が戻ってきました。

部室をどうするか (5 月)

「支援をお願いし、今後様子を見ながら来年 5 月頃にまた再検討する」

さて、その時が来ました。「様子」としては、5 月のいちょう祭 (学祭) も結局中止となり、明らかにアウトでした。しかし、今回はまだ 2020 年度ほど極限状態ではないので、検討の時間がありました。2020 年の流れを受けて、何とか残すことはできないかと、選択肢を洗い出しました。

  • 協賛
  • HP 広告料収入
  • 企業との交渉
  • クラウドファンディング
  • 有償 OB 会の設置

なんというか、無理!!って言いたくなるような選択肢です。協賛では部室維持につながるような額は期待できず、広告料、企業との交渉、クラウドファンディングは余りに難易度が高く、「多さ課題が苦」の部員が十分にそれに貢献できるとも期待できません。有償 OB 会は KMC で実際に採用されているなど有力でしたが、加入メリットの捻出が課題です。しかし、部室を手放すと以下の懸念がありました。

  • 備品置き場所問題
  • 学祭時の商品保管用冷蔵庫
  • 2 度とあの部室を取れないかもしれない
    • 大家さんとの契約更新の際の反応は、学生の入居は騒がしい等で迷惑になったり、老朽化による安全性が心配だったりとあまり積極的ではなかったようで、手放すと立地にしては (家賃的に) 超好条件だったあの部室をもう 2 度と借り直せなくなる恐れがありました

しかし、今回は耳寄り情報がありました。何と、他部の解散により学内の部室 (共用室) に空きができたのです。学外に確保していた部室より遥かに狭い (4m x 3m) ため、活動スペースが取れないばかりか物品も結局ほとんど処分しなければ収まらず、インターネットもなく、共有空間なので高額機器や金銭の保管に少し抵抗が生まれるなどの問題点はありましたが、金がない以上贅沢は言えません。

共用室

  • 共用室に興味を持っている団体は他にもあり、判断を待ってもらえるのは今年限り
  • 収支を計算するとマイナス 40 万超 (年間) なので、仮にクラウドファンディングで 50 万儲けても 1 年しか伸ばせず、もし 100 万儲けられたとしても 2, 3 年延ばすのみ
    • しかもその時点で共用室は取れなくなる
  • 部室維持が困難になったのは COVID-19 のパンデミック前からの話であり、収束しても厳しいことに変わりはない
    • 結構前から、部費半期 1 万円であったことが判明した
    • 部費の高さで入部をためらう声もあり、部室維持のために部員が減るのは本末転倒である

以上の思考の元、部室維持は長期的に非現実的だということを部員一同が認識し、2021 年末を以って部室を手放し、物品は共用室に移送することで合意しました。

しかし、話はこれで終わるほど単純ではありません。大量の物品をどうやって残す/売る/捨てるの判断をするのか、私物が紛れていないか、どうやって運ぶのか、どうやって捨てる/売るのか。そこにこそ、最大の砦が待ち構えていたのです...

活動形態の変更 (8 月)

部室を手放すということは、完全に Discord が活動場所になるということです。そこで、Discord サーバーの整理を始めました。まず、役割の同じチャンネルが複数あったり、全く使われていないチャンネルがあったりと、散らかり放題だった内部サーバーを、実情に合わせて再編しました。そして、「新歓サーバー (公開)」と「メインサーバー (内部)」の 2 つが存在していたのですが、これらの役割を変更し、前者をメインサーバー、後者を議論用サーバーに変更しました。これには、以下の背景があります。

まず、新歓サーバーはその名の通り、2020 年に新歓目的で作成されたサーバーでした。そして、内部サーバーが活動のメインであり、こちらは 2019 年に Slack から移行する形で作成されました。新歓サーバーは、おそらく新歓期間終了後破棄することを想定されていたものですが、新歓時期終了後にも新たな参加者が入ってくることが結構あり、また公開サーバーであるという側面から先述の ANU CSSA の方との交流にも利用され、その結果、破棄されず 2021 年の新歓にも使用されました。しかし、新歓時期にしか使用しないとなると、OUCC の活動風景が唯一見える新歓サーバーは新歓時期を過ぎると閑散とし、これによって新たな参加者も発言しにくくなり、多大な機会損失が生じているのではないかという懸念がありました。

現代では、誰でも参加できる高度なオンラインコミュニティが無数に存在します。部費を払わないと参加できないオンライン活動のクラブは、この点で大きなディスアドバンテージになります。これは OUCC に限らず、多くのクラブ・サークルが直面した問題です。そもそも、私は「それによって価値を失ったクラブ・サークルは、そのまま消えてしまえばいい」という考え方です。これは、OUCC に対しても同じです。しかし、OUCC には「同じ大学のコンピュータに興味のある人と情報交換ができたり、(主に) 情報系学科なら履修や講義の相談ができたりする」という価値を見出していました。なので、そこに重点を置くことを考えました。具体的には、「活動を公開空間で行い、部員以外でも誰でも参加できるようにする。つまり、コミュニティとしての存続を目指す。」ということです。とはいえ無料で運営できる状態ではないので、部として参加する外部イベントへの参加、活動方針の決定、備品利用等の「部員」にのみ提供される付加価値をいくらか残すことで、部費を払って入部するメリットも確保しました。

KC3 2021 (9 月)

KC3 2021 も、2020 年度と同じくオンライン開催となりました。この回も私は受講生側でしたが、当時の部長 (2020 年度とは違う) が JavaFX の講習会「初心者向けGUIプログラミング -某番組のシステムをまねてみよう」を開いていました。

部室の整理 (9-10 月)

部室には大量の物品が存在し、共用室への移動時には大量の処分を行わなければいけないことが分かっていました。そのため、「今どのような物品が存在し、どれが優先度が高いか」を明らかにする必要がありました。ちなみに、「部室をどうするか」から 4 か月も経っているのは、学期中には碌な活動ができないという「多さ課題が苦」ならではの制約によるものです。これが、後に地獄を招くことになるのですが...

さて、まず、整理には 4 日間を確保しました。行う内容は以下です。

  • 必要な物を優先度の高い順に整理する
    • よく分からないもの・判断に迷うものは適宜 OB さんにもメンションしながら頻繁に質問する
  • 明らかなゴミを除き、写真と共に選別結果ごとにリスト化する
    • 大きい物品は採寸結果も
    • 1 つ 1 つのリスト化は非現実的なので、小物類はまとめて写真を撮るだけでもいい
  • 古い PC を起動し、スペックを確認する
    • 使えそうでないものは USB 等にデータの救出を行う
    • 起動しないものは HDD を取り出せばいいと思われるが、大したデータはなくそのまま捨ててもいい
  • コード類はどこに繋がるものかわかればその機器に繋ぐ
    • 最悪まとめて箱詰めでも良い
  • OB さんに原則 25, 26 日に自分の所有物を回収してもらう
    • 都合が合わない方は 11/14 までの範囲で柔軟に調整するが、できる限り早くにお願いする
    • 11/14 までに連絡がないものは、部の備品として処分などの取り扱いを行うことも併せて告知する
  • 業者に運んでもらうものを決める
    • 見積もり訪問が来るので、値段が変わらない範囲を探る

できそうに見えた方も多いことでしょう。実際、我々もできると思って 4 日間としましたから。しかし、現実はそう甘くはありませんでした。

物品量

はい、無理です。まず足の踏み場がないので、整理やリスト化などの作業を行うためのスペースすらなく、リスト化作業もかなり滞りました。遠い昔の世代が遺した得体の知れない物体も大量に存在し、これもまたリスト化作業の停滞を招きました。最終的に、リスト化は「ある程度」進みましたが、「優先度の高い順に整理」は無理でした。結局、学期が始まってからも部員が頻繁に通ってリスト化が済んでいないものをリストに追加する作業が続くこととなりました (自分も 65 枚ぐらい撮りました)。ちなみに、このリストは最終的には 41 ページに渡る巨大文書となりました。

物品リスト

リスト化できたら、これに優先順位をつける作業に入ります。これは、部会で投票方式で行いました。時間が掛かることは分かっていたので、限界まで時間短縮するためにテンポよく短時間の投票を繰り返す方式にしたのですが、それでも 14:00 に開始して 22:30 終了という、過去に例を見ない恐ろしく長丁場な部会となりました。

物品の優先順位投票

引っ越し (10 月)

必要なものが決まれば引っ越しですが、単純に量が多いことと冷蔵庫のような大型物品を含むことから、学生だけでは非現実的で業者に頼むことは必須でした。格安業者は不安だったため、大手の業者で相見積もりを取って決めました。壊れた冷蔵庫 (冷蔵庫は 3 台あり、そのうち 1 つは壊れていた) の処分を含めて 32890 円になりました。家電リサイクル法とかいうクソが絡む物品を含んでいることを考えると、まあ妥当な額です。ちなみに、箱詰め資材が届くのが引っ越しの 2 日前の夜という感じで、そこから (大幅処分の上でも依然) 大量の物品を箱詰めする訳なので、かなりギリギリでした。近くに住んでいる部員が夜遅くまで献身的に働いてくれたことで、辛うじて間に合いました。ブラック労働

しかし、これで引っ越しが完了した訳ではありません。処理しなければならない不要物品が大量に残っているし、さらに事前に伝えておいた冷蔵庫とホワイトボード以外は箱に入るものしか運んでもらえず、結局プリンタなどは部員で運ぶことになりました。この辺のこと、ちゃんと確認しておかないといけないという教訓ですね。まあ、それでも助かったことは間違いないです。

残った物品

まちかね祭 (11 月)

いきなり何の関係もない話題に飛びますが、実際にこのタイミングに学祭が割り込んできたのです。実際には準備があるので、8 月から議論が始まっていました。引っ越しと学祭出展の 2 刀流はもう... しかも、この学祭、食品模擬店とゲーム有償展示の 2 店舗同時出店なんですよね。両方の準備をしなければならないし、何より少ない部員で 2 店舗を準備日含めて 4 日間割り当てないといけない。はっきり言って無謀ですが、この学祭は完全に資金獲得を目的としており、財政がギリギリという背景から、何としてもそうせざるを得なかったのです。

この学祭はただ大変だっただけではなく、2 年ぶりということで、学祭経験者がもはや 0 でした。ゲーム展示の方は、部として持っているゲーム (VR、シューティング) と夏休みに新規開発した占いを出すということで、企画自体はまあ何とか決まりました。問題は食品模擬店です。まず何を売ればいいのか、色々と議論の後に唐揚げに決まった後も何 kg 仕入れればよいのか、調理にあたって必要なものは何か、買い出しはいつどこでどうやればいいのか、全てが手探りでした。あまりに不安要素が大きかったため試食会を部室で開き、調理方法などを確認しました。

そして迎えた本番、COVID-19 が小康状態であったことや久し振りの学祭ということもあって、とんでもない客入りでした。唐揚げは原価率 25% で飛ぶように売れ、ゲーム展示にも長蛇の列ができました。私は食品模擬店を担当しており、ゲーム展示の担当ではなかったのと、現場が限界運営で状況報告をする余裕がなかったようで、全く状況は分からなかったですが、とりあえず盛況だということは間違いなかったようです。

食品模擬店
ゲーム展示

もうとにかく「大変」の 2 文字だった学祭ですが、その効果は絶大でした。売上 249849 円で 180650 円の利益を出したのです。もはや我々にも信じられない数字でした。頑張った甲斐があるというものです (そして当時来てくださったお客さんにはもう感謝してもしきれません)。そしてこれによって得た盤石な財政は、以降の引っ越し作業を大いに救うことになります。

不要物品の処分・売却・譲渡 (11-12 月)

学祭が終われば、不要物品の処理です。この時点で OB さんによる回収期限は終わっていたので、あまりに高価だと判明した物品以外は部員と OB さんが自由に持って帰ってもいいという方針にしました。それでも残ったものを処理する形になります。

まず、粗大ごみは大変です。ゴミ処理場まで運ぶのには車が必要になり、処分費用も掛かります。電化製品なら家電リサイクル法とかいうクソも絡んできます。今回は、CM でおなじみのジモティーを利用し、部室まで引き取りに来てもらうことを前提にタダで出しました。結果、鉄くずを集めているらしい業者っぽい方が全て回収してくれて、この辺の物品は特に大きなテレビ台などを除き無事処理できました。

続いては、金になるもの (書籍、ゲーム、ゲーム機等) の処理方法です。それぐらい残しておいてもいいじゃないかと思うかもしれないですが、量が尋常ではないのです。書籍もゲームも、「それぞれ」200 以上はあったのではないでしょうか。もはや数えることすらできませんでした。ゲーム機も PS2 が 5 機ぐらいあったりと意味不明でした。一体どの世代が何のために... オタクの楽園ではありますが、こんなものを狭い共用室に運べるわけがないので処分が必要になります。金になるなら売りたいというのが、常に財政がギリギリの貧乏クラブの発想で (そもそも、だから部室を引き払うことになったわけで)、ボドゲと一部の漫画はボドゲ研漫研に売りました。しかし、先述の学祭で資金を獲得したことで、「何としてでも金になるものは売らないとまずい」というよりは、「あと 1 か月で引き払わないといけないのにこれでは間に合わない」の方が強くなっていたため、特に高額なもの以外は業者に一括で丸投げする方針になりました。というか、期日が近づくにつれてどんどん適当になっていきました。まあ、本当に売ることを考えるとヤフオクやメルカリになりますが、これはあまりに大変なので非現実的でした。なお、この方針でさえ期日ギリギリだったので、学祭で大失敗していればもう... かなり悲惨な結果だったことでしょう。

大量のゲームソフト
大量のゲーム周辺機器

一応、当時のメモを掲載しておきます:

TODO:

  • 書籍類/ゲームソフトの分類(種類別)、その後シリーズごとに出品用写真を撮り箱詰め。

    • 箱詰めは、部屋に散乱して作業の大きな妨げとなっているため行う。どれがどの箱に行ったか分かるようにすること。
    • 見落とされていた専門書は共用室へ移動エリアに置く。
  • 書籍類/ゲームソフトのうち高価な物を探す。

    • 上の作業で写真が届くのと並行に、現地にいない人が解析すると効率が良さそう。
  • ゲーム機/周辺機器の分類と、出品用写真の撮影。

    • これも、現地にいない人が同時に価格解析すると効率良さそう。
  • PC/モニタ/HHKB/ゲーム系/例のレンジの動作確認。

  • 共用室に運ぶエリアに置いた物品と高価値物品の移送。また、そこにはないがクーラーボックスや座布団と椅子と机(とモニタ)も運ぶ。

  • あまり価値のないゲームソフト/ゲームハード/書籍類/トレカは BOOK OFF か HARD OFF に一括で投げる。

  • 不要な家具類はヤフオクかジモティーで出品する(最悪0円でもいい)。引き取り手が無ければ粗大ごみ。

  • 重要度の低いボドゲをボドゲ研かメルカリ/ヤフオクに売る。

  • 大量のゴミ(粗大ゴミ含む)の処分。

    • 行政サービスを使うか民間業者を使うか。行政サービスなら手続きとかいろいろあるし休日や年末年始は受けてくれない可能性があるし、申込期限もあるので注意。
  • 鍵の取り外しと、マナーとして最低限の掃除。

さて、残りはシンプルに「ゴミ」です。それの量は、「引っ越し」の節の最後に示した画像から察してください。一部、引っ越し業者の箱に入らなかったために部員が自力で運んだ物品、ギリギリになって発掘されたり議論が漏れていたりしたために検討時間が取れなかった物品、処分方法不明な自動車関連の缶系物品など、共用室に運び込んだ物はありますが、ほとんどはゴミです。これを、自治体のゴミ収集で何とかできるでしょうか。そもそも、部員の力で運び出せるでしょうか。...無理です。どう考えても無理です。ということで、ゴミ処理の業者を呼ぶことになりました。業者決めというのは例によって大変なのですが、今回はコネがありました。そう、ジモティーのときに粗大ごみを引き取ってくれた業者です。ここの業者に相談したところ、何と大量の書籍の売却、粗大ゴミの回収を一挙に引き受けてくれるということで、これでようやく先が見えました。さらにこの業者は、粗大ごみ以外の大量のごみも引き受けてくれて、部室が一気に片付きました。軽トラック大盛 2 回分で 24000 円と良心的な価格で、学祭の利益をもってすれば余裕でした。この業者に巡り合えていなければどうなっていたか、考えるだけでも恐ろしいです。ジモティーへの出品がきっかけでこの結果にたどり着いたことを考えると、人生何がどう繋がるか、分からないものですね。

大量処分後の部室

部室の退去 (12 月)

さて、前節の業者によるゴミ回収が 12/18 で、退去日は 12/25 (年末年始休暇に入るので 25 が限度だと大家さんに言われたため) でした。残りの物品も、授業の合間を縫っていくつか運び、退去日の 25 日に何人かで集まって運び終わりました。本当にギリギリですが、何とかなりました。

しかし、最後に事件が起きました。物品移送の後、共用室で棚を組み立てていたときのことです。退去の立ち合いをしていた部員から、連絡が入りました。

2021/12/25 15:07
下駄箱は元からあったものでは無いので運び出してくれと言われました
人手が要ります

怪しいかもしれないとは思いつつ、しかしこれはさすがに備え付けだろうと思って処分しなかった下駄箱が、なんと当日にこう言われた訳です。傘立ての壺があったのですが、これについても同様でした。契約当時を知る OB さんとは既に連絡が取れなくなっており、というか誰かも分からず、大家さんに確認を取るも返事がなく、当日にこの有様... 現場は大混乱でした。契約が切れた以上当日中に撤去する必要があり、しかし共用室に入るサイズではありません。先日の業者も、こんなタイミングでいきなり呼んで来てもらえるとは期待できないし、そもそもこの日は年末ということもあり条件は最悪です。

しかし、どうにかするしかないので、最寄りのリサイクルショップを半ば賭けで目指しました。最寄りとはいえ、大荷物をタイヤがボコボコの台車で徒歩で運ぶとなるとかなり遠いです。何とか着いても、壺は無償引き取りできたものの、やはり下駄箱は無料引き取りもできず、有償処分も引き受けていないと言うことでした。途方に暮れていると、見かねた店員さんが知り合いの業者をあたってくれ、8000 円出せば処分を引き受けてくれる人を見つけてくれました。下駄箱 1 つ 8000 円とは普通の感覚ではぼったくりですが、タイミングがあまりに最悪で他の選択肢は川に投げるしかないという状況にあったため、金を積めばそんな状況を救ってくれるというだけでもものすごく有難かったのです。何なら数万円でも出すつもりでした。というわけで、この条件で引き受けてもらうことにしました。これも、学祭による盤石な資金が後ろ盾にあったためにできた判断であり、先の学祭の成功は重要な鍵になりました。

という訳で、一悶着ありましたが、何とか部室の引っ越しが完結しました。

引っ越し後の共用室

...はい、結局新たなオタクゴミ屋敷になりました。そもそも活動スペースが取れないことは想定内であり、倉庫として運用するつもりだったため自然な結果ではありますが、運び込んだ備品を使うスペースすらないため、物品の保持基準を更に厳しくして断捨離する必要がありそうです。とはいえ、これには 1 年経った今も手を付けられていません。

Advent Calendar (12 月)

毎年恒例 OUCC Advent Calendar 2021 です。今回は部室の引っ越しのクライマックスと被ったことでかなり厳しかったです。その結果、12 記事しか集まらず、それも後半のみという寂しい感じになりました。しかし、OUCC 部員でない方が 1 つ記事を埋めてくれたのには、公開コミュニティ化の手応えを感じました。

ちなみに、私は「テトリスの電子辞書移植」という記事を書きました。締め切り直前に書き始め、ブログシステムのトラブルによって結局間に合わなかった迷作です。

役職再編 (12-1 月)

実際の運営状況を反映し、旧来の役職体系を再編しました。それぞれの責任範囲を明確にし、「これは誰がやるのか」問題を減らそうとも試みました。「これは誰がやるのか」というのが曖昧なものって、結局誰もやらないので私に回ってきていたんですよね。そういう意味でも、運営の健全化において重要でした。なお、部室係はここで廃止したものの、半年後に需要が出てきたため復活させました。

旧:

・部長
・副部長: 部会の司会進行、部の運営・意思決定、他役職のサポート
・国際交流: ANU CSSAとのやり取り
・国内交流: 外部(大学・他企業)との交流(これまでの渉外と同)
・会計: 部のお金の管理
・Webサーバー係: Webページ更新など
・出版係: 部誌・広告作成
・部室係(部室に近い人)
・店舗・副店舗
・展示・副展示

新:

# 新3年配当

部長
- 副部長への職務継承、アドバイス、手助け

店舗
- 学祭の食品模擬店の正責任者
- 総会の出席、検便の回収、シフト決め等を行う

展示
- 学祭の展示の正責任者
- 総会の出席、シフト決め等を行う

# 新2年配当

副部長
- 部の代表者
- 部会などイベントの計画・開催
- 他の人の手が回ってないこと全部

会計
- 会計記録
- 部費徴収
- 部長、副部長、会計による会計会議への参加
- バイトの斡旋
- その他、金銭に関わること

渉外
- 部外の方との橋渡し(郵便受けの確認やメール対応、会議など)
- 教室確保(共用室の利用届更新を含む)

Intl PR
- ANU CSSA とのやり取りを含む国際交流
- 留学生のサポート

広報
- Twitter (@OUCC, @OUCC1) や OUCC BLOG の対応と情報発信
- 新歓(サーオリを含む)の取り仕切り

サーバ
- サーバの管理・運用
  - セキュリティ更新とクラッキング被害有無の確認/対応
  - bot の管理
- OUCC が管轄する GitHub, GitLab, Discord, Slack, etc の管理
  - これらの Owner 権限あるいは類する最高権限を持つことを想定

出版デザイン
- Web サイトの管理・更新(部誌制作の代わり)
- 紹介動画制作、サーオリ/学祭等のサムネイル/ビラ制作

部費システムの大幅変更 (2 月)

まず、何度か軽く触れましたが、当時は

  • 春夏学期・秋冬学期それぞれ、会計が決めた額の部費を支払う
    • 大阪大学は形だけの 4 学期制なのでこの呼び方になりますが、前期・後期と読み替えても支障はありません
  • 新規入部者は「入部金」という、部費よりも安く設定した料金を支払うことで参加でき、これを当期の部費納入と見做す

というシステムでした。そして、その部費は部室の家賃を賄うために 15000 円まで跳ね上がっていたのでした。

しかし、月 5 万の部室を手放した今、そこまで部費を吊り上げる必要はありません。もっと安価にできます。そして、半期単位での集金は会計への大きな負担になっていました。この時点で出費を考えると、

  • サーバレンタル料
  • ドメイン (oucc.org) 代
  • 学祭費用

でした。学祭費用は大抵の場合学祭の利益で相殺されることと、却って利益になることの方が多いため考えないことにすると、残りは年間 3 万円程度でした。ということは、全体で年間 3 万円を部費として徴収できれば、OUCC は安定して存在できる訳です。そこで、部員 15 人は確保できるだろうと見込み、以後固定で年間 2000 円とすることにしました。今まで年額に直すと 3 万円前後徴収していたことを考えると、超大幅値下げです。部費はオープンコミュニティ化における大きな障壁だったため、この値下げは重要でした。また、有効期間を厳密に年度単位 (4/1 - 3/31) としたことで手続きも分かりやすくなり、この期間に部費を納入した人数として部員数の正確な統計も得られるようになりました。

まとめ

2021 年度の「活動」は、まあ見ての通り、ほとんど「部室」でした。折角沢山入ってきてくれた新入部員に労働しか与えられなかったという点は申し訳ないなぁという気持ちですが、こうする他ありませんでした。改めて、自分の世代で出した遺産の処理をその世代のうちに済ませておくことの大切さを感じました。

一方で、運営関連では雑だった部分を是正したり、公開コミュニティ化したりと、時代に合わせた大幅な改革を行いました。これによって入部できない人も活動に参加してくれるようになったりと、一定の手応えは感じています。公開コミュニティ化については、2022 年現在においても不十分なところはあります。例えば、課題の質問は剽窃や講義の規則 (第三者に資料を渡してはいけない etc) 等の懸念があったり、単に恥ずかしかったりで公開サーバーでは行いづらく、結局内部サーバーでの会話の方が多くなってしまっている現状があります。また、公開コミュニティ化する上では開発プロジェクトは OSS 化したいものですが、外部公開を想定して開発していなかったためにソースコードがぐちゃぐちゃだったり、著作権周りが怪しかったりで開発に携わった部員が消極的なことも多く、既存プロジェクトについてはあまり実現しませんでした。この辺をより推進するのは、2022 年度以降に託されました。

中編はここまでです。続きは OUCC の 3 年間を振り返る [後編] にお進みください。