REAPERのプロジェクトタブを切り替えるReaScriptを書いたら車輪のアンコールだった

# 概要

REAPERのプロジェクトタブを、WebブラウザみたいにCtrl+Tabでサクサク切り替えられたらな～～。
→ でもアクションリストにはなぜかそういうアクションが見当たらないんだよな～～。
→ よし作るか。
→ 作った。
→ あっよく見たらアクションリストに元からちゃんとそういうのあった。
→ 車輪の再発明おめでとう。永年車庫入り。死。
→ いや、死なすぐらいならReaScript基礎編記事としてQiitaにでも突っ込むか。←今ここ

# 実装

とりあえず一番のコア部分はこれ。

```lua:lib\Select&nbsp;project&nbsp;tab.lua（下にも再掲します）
function SelectProjectTabByStep(step)

  local function prj(i) return reaper.EnumProjects(i, "") end
    
  local i, i_active = -1, -1
  local prj_active = prj(-1)
  repeat
    i = i + 1
    i_active = prj_active==prj(i) and i or i_active -- 今のプロジェクトが何番目のタブなのか把握
  until not prj(i+1)
  local i_max = i
  local i_new = (i_active+step) % (i_max+1) -- インデックス外なら回り込む
  reaper.SelectProjectInstance(prj(i_new))

end
```

使ったReaScript関数は2つ。

+ reaper.EnumProjects
+ reaper.SelectProjectInstance

## reaper.EnumProjects

```
ReaProject retval, string projfn = reaper.EnumProjects(integer idx, string projfn)
```
> idx=-1 for current project,projfn can be NULL if not interested in filename. use idx 0x40000000 for currently rendering project, if any.

`idx`に何番目のプロジェクトかを指定すると、`ReaProject`とかいうデータ型（内蔵デバッガの表示は`reaper object`）でそのプロジェクトが帰ってくる。
インデックスは一番左のタブから0始まりで数え、また特別に現在開いているプロジェクトは`-1`で指定できる。…というか、インデックス範囲より上だったり下だったりするような、はみ出した数を入れたらとにかく現在のものになるっぽい。なのでインクリメントして端まで達しても勝手に回り込んだりはしてくれない。あと少なくとも整数型である必要があるらしく、色々試したけど整数以外はエラーになった。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/225134/f364e353-6725-7b3b-bd09-f2a61d1883c3.png)

```lua
-- 上記のような状態で、
prj = reaper.EnumProjects(-1, "")    -- 現在のタブ（2個目のタブ）
prj = reaper.EnumProjects(0, "")     -- 1個目のタブ
prj = reaper.EnumProjects(-3776, "") -- 現在のタブ（-3775個目のタブは無いので）
prj = reaper.EnumProjects(3.14, "")      -- "number has no integer representation"
prj = reaper.EnumProjects("Mt.Fuji", "") -- "number expected, got string"
prj = reaper.EnumProjects(nil, "")       -- "number expected, got nil"
```

`projfn`はファイル名だけど今回は関係ない。
なお[リファレンス](https://www.reaper.fm/sdk/reascript/reascripthelp.html#EnumProjects)には`projfn can be NULL`とあるが、少なくとも文字列型になり得る値である必要があるらしく、サボったり`nil`にしたりするとエラーになった。

```lua
prj = reaper.EnumProjects(-1, "")   -- OK
prj = reaper.EnumProjects(-1, 3776) -- OK
prj = reaper.EnumProjects(-1)      -- "string expected, got no value"
prj = reaper.EnumProjects(-1, nil) -- "string expected, got nil"
```

## reaper.SelectProjectInstance

```
reaper.SelectProjectInstance(ReaProject proj)
```
REAPERのプロジェクトを指定するとそのインスタンスを開く。

なお、ReaScriptでは何をするにしてもほぼ必ずこの`ReaProject`を指定する。というか、例えばメディアをいじるためにはまずそのメディアの取得が必要になり、そのためにはそのメディアがあるトラックを取得し、そのためにはそのトラックがあるプロジェクトを取得……という感じで、何をするにも結局親玉のプロジェクトを掴んでやらないことには始まらない仕組みになっている。気がする。
で、実際そのためにいちいち`ReaProject`型データを頑張って掴むのはめんどいので、特別に`0`を入れてやると自動的に現在のアクティブなプロジェクトを指定することができるようになっている。例えばプロジェクトのメディアアイテム数を数える`reaper.CountMediaItems`の[説明を見る](https://www.reaper.fm/sdk/reascript/reascripthelp.html#CountMediaItems)と、

```
integer reaper.CountMediaItems(ReaProject proj)
```
> count the number of items in the project (proj=0 for active project)

とある通り、便利。
…色々試したところ、実は`0`に限らず割とどんな雑な値でもとにかく現在開いているプロジェクトが指定されるっぽい。まあ`0`にせよと言われてるんだから素直にそうすれば良いんだけど。

```lua
i = reaper.CountMediaItems(reaper.EnumProjects(0, "")) -- 1個目のタブ
i = reaper.CountMediaItems(0)         -- 現在のタブ（推奨）
i = reaper.CountMediaItems(3776)      -- 現在のタブ
i = reaper.CountMediaItems(3.14)      -- 現在のタブ
i = reaper.CountMediaItems("Mt.Fuji") -- 現在のタブ
i = reaper.CountMediaItems(nil)       -- 現在のタブ
i = reaper.CountMediaItems()          -- 現在のタブ
```

あと、この`proj=0 for active project`という説明は、必ずしも全ての`ReaProject`型引数を要する関数の説明に書かれているわけでもなければ、基礎事項としてリファレンス全体の頭とかに書かれているわけでもない。
なので、**うっかりド忘れすると困る**（困った）。今回の`reaper.SelectProjectInstance`の項目にも[そういう説明はない](https://www.reaper.fm/sdk/reascript/reascripthelp.html#SelectProjectInstance)ため、あらかじめ知っておく必要がある。

参考までに、Chromeで開いた[Lua版リファレンスページ](https://www.reaper.fm/sdk/reascript/reascripthelp.html#l)内を`0 for active project`で検索したら結果は10件、`ReaProject proj`で検索したら106件だった。
つまり、`ReaProject`型の引数を要するAPIが106件あって、その中で上記の「`0`にすれば済むよ」という話を説明してくれているのは10件だけということ。頑張ろう。


## あとは書く

REAPERリソースフォルダ（`REAPER.ini`があるフォルダ）の`Script`フォルダ内に自分用のフォルダ`Phroneris`（私の横文字HNです）を作り、更にその中に`lib`フォルダを作って複数のスクリプトで共用するスクリプトの置き場とした。

```lua:lib\Select&nbsp;project&nbsp;tab.lua（再掲）
function SelectProjectTabByStep(step)

  local function prj(i) return reaper.EnumProjects(i, "") end
    
  local i, i_active = -1, -1
  local prj_active = prj(-1)
  repeat
    i = i + 1
    i_active = prj_active==prj(i) and i or i_active -- 今のプロジェクトが何番目のタブなのか把握
  until not prj(i+1)
  local i_max = i
  local i_new = (i_active+step) % (i_max+1) -- インデックス外なら回り込む
  reaper.SelectProjectInstance(prj(i_new))

end
```

そして以下の2つをアクションから読み込んでショートカットを割り当てて完了。
あっそうそう、`lib`からスクリプトを引っ張ってくるために`reaper.GetResourcePath`で[REAPERのリソースフォルダのパスを取得する](https://www.reaper.fm/sdk/reascript/reascripthelp.html#GetResourcePath)。

```lua:Phroneris_Switch&nbsp;to&nbsp;next&nbsp;project&nbsp;tab.lua
-- こいつにCtrl+Tabを割り当て（既存の重複ショートカットは解雇）
package.path = reaper.GetResourcePath().."\\Scripts\\Phroneris\\lib\\?.lua"
require "Select project tab"
SelectProjectTabByStep(1)
```

```lua:Phroneris_Switch&nbsp;to&nbsp;previous&nbsp;project&nbsp;tab.lua
-- こいつにCtrl+Shift+Tabを割り当て（既存の重複ショートカットは解雇）
package.path = reaper.GetResourcePath().."\\Scripts\\Phroneris\\lib\\?.lua"
require "Select project tab"
SelectProjectTabByStep(-1)
```

ちなみに実際のコードには日本語コメントは一切含めていない。REAPERのスクリプトエディタは、マルチバイト文字（非ラテン文字？）をまともに表示できないので…。

# 成果

![prjtab4.gif](https://qiita-image-store.s3.amazonaws.com/0/225134/f7e46e90-562e-6103-bfec-2fb5287023ab.gif)
（キー入力表示：[tinyosdkey](https://github.com/mieki256/tinyosdkey/releases)）


# 作った動機と顛末

あれは、自作の[製品版REAPER日本語化パッチ（森）](https://github.com/Phroneris/ReaperJPN-Phroneris/tree/master)がver 5.70.001の頃――

「REAPERのプロジェクトタブを、WebブラウザみたいにCtrl+Tabでサクサク切り替えられたらな～～」
「まあ普通に考えてそんな基礎的なアクションはまず間違いなく内蔵してるはずだから、探してショートカット割り当てれば良いよな。アクションリストで…`next tab`っと」

![image.png](https://qiita-image-store.s3.amazonaws.com/0/225134/7d5112f0-20ff-74fb-88e9-4240b29e8678.png)

「…？ `Docker:`っつってるしこれはドックの方のタブだよね。こんだけ？」
「え、じゃあ`tab`ならタブ関連の全部のアクションが出るかな」

![image.png](https://qiita-image-store.s3.amazonaws.com/0/225134/8a6eeaeb-ca42-ede0-6264-3d8ca5701fed.png)

「……？？」

「**プロジェクトタブの切り替え、無いの？？？？？？？**」

​
![image.png](https://qiita-image-store.s3.amazonaws.com/0/225134/8ce7aba3-2ad8-d5db-8143-1167f4f75fbc.png)

「まあ……無いなら作るか」

​
そして完成。

「できた～。さて、それはそれとしてREAPER日本語化パッチの翻訳も進めるか」
「おや？ 気になるブログが」
「ん～なになに、現行の日本語化パッチでは[アクションリストが中途半端にローカライズされるので内蔵アクションを探し難くなる](http://peloreaper.blog.jp/archives/53075150.html)…？」
「…………」

![prjtab5-1.png](https://qiita-image-store.s3.amazonaws.com/0/225134/2775dab4-d66c-a967-0264-2363927c5215.png)

​

​

![prjtab5-2.png](https://qiita-image-store.s3.amazonaws.com/0/225134/1e232355-43f4-0b55-a2d9-28abaf5c8131.png)

​

​

​

​

（日本語化パッチver5.70.002で英語検索に対応させました）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/225134/a13faae5-9fe4-aef9-a3bd-e089f52d93ce.png)

おしまい。


# 執筆時点の環境

+ Windows 7 64bit
+ REAPER v5.78/x64, v5.79pre7/x64
+ REAPER APIリファレンス Generated by REAPER v5.70/linux-arm


# リンク

[REAPER | ReaScript](https://www.reaper.fm/sdk/reascript/reascript.php)
[REAPER API functions](https://www.reaper.fm/sdk/reascript/reascripthelp.html)
[REAPERの日本語化パッチのお話 : ペロリーパー | PeloREAPER](http://peloreaper.blog.jp/archives/53075150.html)
[製品版REAPER日本語化パッチ（森）](https://github.com/Phroneris/ReaperJPN-Phroneris/tree/master)