【SuperCollider】ブラウザ(Chrome)でSuperColliderを動かす

# はじめに

[SuperCollider](https://supercollider.github.io/)はオーディオ合成とアルゴリズム作曲のためのソフトウェアで、独自の言語でシンセの自作などできます。

MacOS, Windows, Linuxで動作するのと、音質も良いため実際の作曲にも有用です。自分もSuperColliderのライブコーディングによって曲つくっているので是非聴いてください！

<iframe width="100%" height="450" src="https://www.youtube.com/embed/X8BImD-Xwq0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 課題

ただ、このコードと一緒に音が鳴っているカッコ良さを他の人に体感してもらうためには、現状だと各環境に合わせてSuperColliderの環境構築をしてもらわなければならず、なんとかブラウザで共有できないかと考えていました。

# SuperCollider runs in the browser!

そんなことを考えながらいろいろGithubなど徘徊していたらまさにな記事を見つけました。

- [SuperCollider runs in the browser](https://www.reddit.com/r/supercollider/comments/kpnidv/supercollider_runs_in_the_browser/)
- [WebAssembly support](https://scsynth.org/t/webassembly-support/3037)


Scissさんという方がWebAssemblyを用いてブラウザでもSuperColliderが動作(エディタは別で、再生部分のみ)するという画期的なことをしてくれていました。実際に彼のサイトで試せます。(Chromeで開いてください)

[https://www.sciss.de/temp/scsynth.wasm/](https://www.sciss.de/temp/scsynth.wasm/)

以下の手順で再生が確認できるかと思います。

1. Bootボタンクリック
1. F12(検証ツールを開く)
1. Console
1. Consoleに`d_bubbles()`と打ち込みEnter(bubblesというシンセのパターン定義)
1. Consoleに`s_bubbles()`と打ち込みEnter(bubblesを再生)
1. Consoleに`cmdPeriod()`と打ち込みEnter(再生停止)


# WebAssemblyでSuperColliderをbuildする

[README_WASM.md](https://github.com/Sciss/supercollider/blob/89c431ad61adb5298a0086148e2191119f6824e7/README_WASM.md)にしたがってbuildし、localhostでさきほどのサイトを確認するところまでいきましょう。

ちなみに環境は

```
OS: MacOS
Python: 3.8
```
です。

## emcriptenのinstall

githubをcloneしてくる適当なディレクトリに移動してから以下のコマンドを実行

```sh
# Get the emsdk repo (ちなみに自分がcloneしたコードのコミットハッシュは060b48f0c69827f95c8433ee965656babf953e82でした)
git clone https://github.com/emscripten-core/emsdk.git

# Enter that directory
cd emsdk
# installできるversion確認
./emsdk list
# Download and install 2.0.13 (lastestでもよかったですが、このREADMEpush時点でのversionを取得しています)
./emsdk install 2.0.13
# Make the "2.0.13" SDK "active" for the current user. (writes .emscripten file)
./emsdk activate 2.0.13

# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh
```

## SuperColliderをwasm(WebAssembly)でbuild

上でemcriptenをinstallしたターミナルをそのまま用います

```sh
# githubをcloneしてくる適当なディレクトリに移動
cd ../
# supercolliderのリポジトリをclone(自分のclone時点でのコミットハッシュは89c431ad61adb5298a0086148e2191119f6824e7でした)
git clone git@github.com:Sciss/supercollider.git
# supercolliderディレクトリに移動
cd supercollider
# サブモジュールを取得
git submodule update --init --recursive
# build
mkdir build
cd build
../wasm/build.sh
```

## Running

```sh
# wasmディレクトリに移動
cd ../wasm/
```

ここで、READMEの通りだと`python -m SimpleHTTPServer`(python3系なら`python -m http.server`)を実行してhttp://localhost:8000/ にアクセスすればよさそうですが、それだとSharedArrayBuffer is not definedというエラーがでて怒られます。(Chrome92以降で起こるエラーです。詳しくは[こちら](https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation)を確認してみてください)

その対策としてレスポンスヘッダーに

```
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
```

を追加してあげる必要があるので、このディレクトリに以下の.pyファイル(server.py)を追加します。

```python:server.py
import copy
import datetime
import email.utils
import html
import http.client
import io
import mimetypes
import os
import posixpath
import select
import shutil
import socket  # For gethostbyaddr()
import socketserver
import sys
import time
import urllib.parse
import contextlib
from functools import partial
from http import HTTPStatus

from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler


class MyServerHandler(SimpleHTTPRequestHandler):

    def send_head(self):
        """Common code for GET and HEAD commands.
        This sends the response code and MIME headers.
        Return value is either a file object (which has to be copied
        to the outputfile by the caller unless the command was HEAD,
        and must be closed by the caller under all circumstances), or
        None, in which case the caller has nothing further to do.
        """
        path = self.translate_path(self.path)
        f = None
        if os.path.isdir(path):
            parts = urllib.parse.urlsplit(self.path)
            if not parts.path.endswith('/'):
                # redirect browser - doing basically what apache does
                self.send_response(HTTPStatus.MOVED_PERMANENTLY)
                new_parts = (parts[0], parts[1], parts[2] + '/',
                             parts[3], parts[4])
                new_url = urllib.parse.urlunsplit(new_parts)
                self.send_header("Location", new_url)
                self.send_header("Content-Length", "0")
                self.send_header('content-type', 'text/html')
                # add header
                # https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
                self.send_header(
                    'cross-origin-embedder-policy', 'require-corp')
                self.send_header('cross-origin-opener-policy', 'same-origin')
                self.end_headers()
                return None
            for index in "index.html", "index.htm":
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                return self.list_directory(path)
        ctype = self.guess_type(path)
        # check for trailing "/" which should return 404. See Issue17324
        # The test for this was added in test_httpserver.py
        # However, some OS platforms accept a trailingSlash as a filename
        # See discussion on python-dev and Issue34711 regarding
        # parsing and rejection of filenames with a trailing slash
        if path.endswith("/"):
            self.send_error(HTTPStatus.NOT_FOUND, "File not found")
            return None
        try:
            f = open(path, 'rb')
        except OSError:
            self.send_error(HTTPStatus.NOT_FOUND, "File not found")
            return None

        try:
            fs = os.fstat(f.fileno())
            # Use browser cache if possible
            if ("If-Modified-Since" in self.headers
                    and "If-None-Match" not in self.headers):
                # compare If-Modified-Since and time of last file modification
                try:
                    ims = email.utils.parsedate_to_datetime(
                        self.headers["If-Modified-Since"])
                except (TypeError, IndexError, OverflowError, ValueError):
                    # ignore ill-formed values
                    pass
                else:
                    if ims.tzinfo is None:
                        # obsolete format with no timezone, cf.
                        # https://tools.ietf.org/html/rfc7231#section-7.1.1.1
                        ims = ims.replace(tzinfo=datetime.timezone.utc)
                    if ims.tzinfo is datetime.timezone.utc:
                        # compare to UTC datetime of last modification
                        last_modif = datetime.datetime.fromtimestamp(
                            fs.st_mtime, datetime.timezone.utc)
                        # remove microseconds, like in If-Modified-Since
                        last_modif = last_modif.replace(microsecond=0)

                        if last_modif <= ims:
                            self.send_response(HTTPStatus.NOT_MODIFIED)
                            self.end_headers()
                            f.close()
                            return None

            self.send_response(HTTPStatus.OK)
            self.send_header("Content-type", ctype)
            self.send_header("Content-Length", str(fs[6]))
            self.send_header("Last-Modified",
                             self.date_time_string(fs.st_mtime))
            self.send_header('content-type', 'text/html')
            # add header
            # https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
            self.send_header('cross-origin-embedder-policy', 'require-corp')
            self.send_header('cross-origin-opener-policy', 'same-origin')
            self.end_headers()
            return f
        except:
            f.close()
            raise


if __name__ == '__main__':
    MyServerHandler.extensions_map['.wasm'] = 'application/wasm;charset=UTF-8'
    server = HTTPServer(('', 8000), MyServerHandler)
    server.serve_forever()

```

ここで


```sh
python server.py
```

を実行し、http://localhost:8000/ にアクセスすればhttps://www.sciss.de/temp/scsynth.wasm/ と同様にブラウザでSuperColliderのシンセを再生できるはずです！！

<iframe width="100%" height="450" src="https://www.youtube.com/embed/ijmnULvR16I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 現状の課題と今後

[READMEのoverview](https://github.com/Sciss/supercollider/blob/89c431ad61adb5298a0086148e2191119f6824e7/README_WASM.md#overview)にも記載がある通り、現状(2022年1月現在)はまだ音声ファイルを使用できなかったり、Chromeでしか動かなかったり様々な問題はありますが、この[プルリク](https://github.com/supercollider/supercollider/pull/5309)がマージされれば開発が進んでいきそうな気がします。

ちなみに動作しない原因はSharedArrayBufferなので、SharedArrayBufferが動作するブラウザとそのバージョンについてはこちらを参照してください。
https://caniuse.com/sharedarraybuffer

また現状ではOSCを受け取ってその情報に合わせて音を再生する仕様なので、エディタ部分は[supercolliderjs](https://github.com/crucialfelix/supercolliderjs)などを用いて実装すれば良さそうです。

ちなみにですが、単純にSuperColliderのようなことをブラウザでやりたい、という要望に対しては既にflockingjsというフレームワークが存在するので、いまからflockingjsを極めるというのも一つの手かもしれません。

[https://flockingjs.org/](https://flockingjs.org/)