オンライン同期可能なシーケンサーを作った話 前編

少し前になるが、また音の出るおもちゃを作ったので記録を残す。
長くなりそうなので前編/後編に分ける。

作ったもの

DEMO
ソースコード

  • ブラウザで動くマルチトラックシーケンサ
  • 作成中の楽曲をオンライン保存できる
  • URLシェアにより、別のユーザと楽曲の同時編集ができる
    • 編集内容は複数クライアント間でリアルタイム同期される

f:id:nishaya:20180906180720p:plain

使ったもの

  • React/Redux
  • Firebase
    • Cloud Firestore
    • Authentication
    • Hosting
  • TypeScript
  • Web Audio API

発表資料

speakerdeck.com

モチベーションと課題

Reactで音の出るおもちゃを作るのが好きなので、Reactでシンセサイザーを作ったり、Reactでパターンシーケンサを作ったりしてきた。

今まで作ってきたものは音を出したり、パターンの変化を楽しんだりとどちらかといえば即興性の高いものだったが、「曲を作る」となると保存しておき続きを作り進められることや、他者と協力して作曲ができることが求められるだろうと考え、その検証として今回はオンライン保存、同期編集が可能なシーケンサを作るに至った。

  • ブラウザ上で音を鳴らせる
  • タイミング通りにシーケンス(作成した曲データ)を鳴らせる

という要件については今までの成果物で検証できていたので、今回は

  • ブラウザ上でシーケンスをグラフィカルに編集できる
  • 編集したものをオンライン保存できる
  • 保存したものをシェアして複数のクライアントから同時編集できる

といった点にフォーカスを当てて実装を行った。

完成したものについてはデモを触ってみていただきたいが、実装を進める中で発見のあった箇所について書きたいと思う。

ピアノロールの実装

f:id:nishaya:20180906180930p:plain

シーケンス編集の手段としては、一般的なDAW(音楽制作ソフトウェア)なら大抵備えているピアノロールを採用した。
ピアノロールはx軸で時間、y軸で音階を表現するUIだ。

探してみると、Reactでピアノロールを実装している例はいくつかあったものの、そのまま利用できそうなものがない、理想とするピアノロールと違うといった理由から、自前で実装することにした。

ちなみに、私が理想とするピアノロールは、かつて存在したOpcode社のEZ Visionというシーケンサのものなので、今回作ったものもそれに似た挙動になっている。

SVG as JSX

ピアノロールの実装はSVGをReact Componentに内包する形で実装した。
SVGのタグをJSXの中で使うことで、以下のようなメリットが得られた。

  • 図形のグループをReact Componentに切り出しやすい
  • svg elementを普通に記述するとそれなりに多くのattributesを渡す必要あるが、Spread Attributesを使うとシンプルに書ける

例):

<rect {...rectProps, x } />

また、クライアント座標とは別の座標系を扱うことができるので、音楽のコンテキストにマッチした座標を使えるといったメリットもある。

  • 例: 1小節あたりのclient widthが137pxといった半端な値でも、SVG内ではwidthを480とし、端数を出さずに16分割や12分割を行う(=16分音符や3連8分音符を表現する)ことができる
  • client座標 -> SVG座標への変換には以下のようなメソッドを用意した
mouse2svgPoint(e: MouseEvent): SVGPoint {
  const pt = this.svgElement.createSVGPoint()
  const matrix = this.svgElement.getScreenCTM()
  if (!matrix) {
    pt.x = 0
    pt.y = 0
    return pt
  }
  pt.x = e.clientX
  pt.y = e.clientY
  return pt.matrixTransform(matrix.inverse())
}

FirebaseやTypeScriptについては後編で書く

nomusiclife.hatenablog.jp

Ant Designのカスタマイズ内容をStorybookに反映するための設定

Ant Design(antd)のテーマをカスタマイズした内容をStorybookに反映する場合、少しややこしい設定が必要になるのでメモ

前提

  • プロジェクト本体はcreate-react-appで作成
  • TypeScriptを使用
  • antdのバージョンは3.7.3

設定方法

例: デフォルトのfont-familyを変更したい場合

antdTheme.js

module.exports = {
  '@font-family': '-apple-system, Sans-Serif'
}

.storybook/webpack.config.js

const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js')
const tsImportPluginFactory = require('ts-import-plugin')
const modifyVars = require('../antdTheme')
const path = require('path')

module.exports = (baseConfig, env) => {
  const config = genDefaultConfig(baseConfig, env)

  config.module.rules.push({
    test: /\.tsx?$/,
    exclude: /node_modules/,
    include: [/src/],
    loader: 'ts-loader',
    options: {
      getCustomTransformers: () => ({
        before: [
          tsImportPluginFactory({
            libraryName: 'antd',
            libraryDirectory: 'lib',
            style: true,
          }),
        ],
      }),
    },
  })
  config.resolve.extensions.push('.ts', '.tsx')

  config.module.rules.push({
    test: /\.less$/,
    use: [
      {
        loader: 'style-loader',
      },
      { loader: 'css-loader' },
      {
        loader: 'less-loader',
        options: { modifyVars },
      },
    ],
    include: path.resolve(__dirname, '../src/', '../node_modules/'),
  })

  return config
}

Apollo ServerとNowで作るGraphQLモックサーバ #4 モニタリング編

前回の続き。

nomusiclife.hatenablog.jp

作成したGraphQLサーバをモニタリング可能な状態にする。

Apollo Engine

Apollo Engine | The GraphQL Gateway with essential features like caching and performance tracing.

  • GraphQL caching
  • Query execution tracing
  • Error tracking
  • Trends

などの機能を備える

  • モニタリングしたいだけなのでcachingは今回使わない
  • アラートは有償版でのみ有効
  • 有償版は$99/mo~(割と高い)

導入

最初にGraphQLのスキーマをアップロードする

npm i -g apollo
apollo schema:publish --endpoint=<your graphql endpoint here> --key="<your key>"

次に、App Engineを利用するためのセットアップを行う

API Reference: apollo-server | Apollo Server

Provided the ENGINE_API_KEY environment variable is set, the engine reporting agent will be started automatically.

apollo-serverは環境変数 ENGINE_API_KEY がセットされていれば、 その値を使ってapollo-engineを有効化するようになっている。

環境変数.env に記述したものを nowの --dotenv オプションに渡してセットする。

ENGINE_API_KEY=service:foo-bar-baz:xxx...
$ now --dotenv=.env.production
$ now alias

確認

Usage Metrics

デプロイされた環境にアクセスし、Playgroundでいくつかクエリを実行した後、 Apollo Engineの "Metrics" の項目を見ると実行したクエリのログが確認できる。

  • リクエストの頻度
  • パフォーマンスの概要
  • エラー率

f:id:nishaya:20180904081654p:plain

クエリ名ごとにさらに詳細な情報を確認することもできる。

f:id:nishaya:20180904081742p:plain

Daily Report

Slack integrationを設定しておくとdaily reportでモニタリングのサマリを受け取ることもできる。

f:id:nishaya:20180904081807p:plain

Schema Explorer

Schema Explorerではschemaに含まれるtypeの一覧と、それぞれの使用状況を確認することができる。

f:id:nishaya:20180904081821p:plain

例えば、これを見るとPerson.addressは全く使われていないことがわかるので @deprecated にする、といった判断に使える。

Schema History

schemaの変遷を確認できる。 例えば、Person.addressを @deprecated に変更した場合、以下のように表示される。

f:id:nishaya:20180904081832p:plain

まとめ

  • 小一時間くらいあればここまで準備できる
  • あとはSchema足していってResolverをひたすら書く
    • とりあえずクライアントの動作を確認するためだけのモックサーバならmock書いていくだけでもOK
  • GraphQLのサーバ運用するのはそれなりに労力かかる(と聞く)が、モニタリングあればただ闇雲にやるよりはずっと安心

Apollo ServerとNowで作るGraphQLモックサーバ #3 デプロイ編

前回の続き

nomusiclife.hatenablog.jp

ZEIT Nowにデプロイする

Deploying Node.js Apps - ZEIT Documentation

詳しい方法は公式ドキュメントに譲るとして...

一般的なnode appは特に設定をしなくてもプロジェクトディレクトリで now コマンドを実行すればデプロイされ、利用可能な状態になる。
npm scriptsの "start" がエントリーポイントになるので、以下のように設定しておく。

  "scripts": {
    "start": "ts-node src/index.ts"
  },

now.json

また、設定なしでデプロイした場合URLにプロジェクトディレクトリ名が勝手に入ってしまうため、 now.jsonの "name" で明示的にプロジェクト名を設定しておくとよい。

{
  "name": "graphql-server",
  "env": {
    "NODE_ENV": "production"
  }
}

これにより https://graphql-server-foobarbaz.now.sh といったURLにデプロイされるようになる。

GitHubへのpushにフックしてデプロイさせる

手順は以下の通り ZEIT – Now + GitHub

Pull Requestをopenしてブランチをpushするとnow.jsonの設定にしたがってデプロイが始まる。

f:id:nishaya:20180824115112p:plain

デプロイが完了すると下記のようなメッセージが表示され、 commit messageの横にある✓のアイコンからデプロイされた環境にアクセスできるようになる。

f:id:nishaya:20180824115035p:plain

URLを固定する

通常 now でデプロイするとURLが毎回変わってしまう。

デプロイする度にAPIのURLが変わってしまうのは困るので、URLを固定するためにaliasを使う。

Aliases and Domains - ZEIT Documentation

{
  "name": "graphql-server",
  "alias": "graphql-server-foobar.now.sh",
  "env": {
    "NODE_ENV": "production"
  }
}

now.jsonに "alias" として、[任意の文字列].now.sh を追加し、 now alias を実行すると、現在のdeploymentが指定したaliasでアクセスできるようになる。


モニタリング編に続く

nomusiclife.hatenablog.jp

Apollo ServerとNowで作るGraphQLモックサーバ #2 実装編

モックサーバの実装

前回

nomusiclife.hatenablog.jp

1. 型の定義を行う

import { ApolloServer, gql } from 'apollo-server'

const typeDefs = gql`
  type Person {
    id: ID
    name: String
    phone: String
  }

  type Query {
    people: [Person]
  }
`

const mocks = {
  Query: () => ({
    people: () => new MockList([2, 5]), // 2~5件の[Person]が返る
  }),
}

const server = new ApolloServer({
  typeDefs,
  mocks,
  playground: true,
})

const port = 4004

server.listen(port).then(({ url }: { url: string }) => {
  console.log(`Server ready at ${url}`)
})

型定義をしただけでQueryの中身(resolver)を書いていないのだから このままでは動かないんじゃないか...と思うが動く。

Apollo ServerはMockingの仕組みによってresolverが設定されていなくてもそれらしい値を返すようになっている。

2. devサーバを起動

package.jsonに以下のscriptを追加して yarn dev で起動する

  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts"
  },

ts-node-devの --respawn オプションにより、ファイルが更新されると実行されているサーバが自動的に再起動する

3. Playgroundで確認

起動したサーバの / にアクセスするとPlaygroundが表示される。 こういうのが最初から用意されているのもapollo-serverのいいところ。

f:id:nishaya:20180821172302p:plain

一応値は返ってきているが、nameやphoneが全て "Hello World" なのが気になる

4. Mockを調整

nameは名前っぽい、phoneは電話番号っぽい値が返ってほしいので、 mockの設定をもう少し弄ってみる

「っぽい」値を得るためにfakerを使う

import * as faker from 'faker'

const typeDefs = gql`
  scalar Name
  scalar Phone

  type Person {
    id: ID
    name: Name
    phone: Phone
  }

  type Query {
    people: [Person]
  }
`

const mocks = {
  Name: () => faker.name.findName(),
  Phone: () => faker.phone.phoneNumber(),
  Query: () => ({
    people: () => new MockList([2, 5]), // 2~5件の[Person]が返る
  }),
}

f:id:nishaya:20180821172316p:plain

それっぽい値が返ってくるようになった


デプロイ編に続く

nomusiclife.hatenablog.jp

Apollo ServerとNowで作るGraphQLモックサーバ #1 準備編

GraphQLのモックサーバを作る際に使うものをメモ

使うもの

  • TypeScript
  • ts-node, ts-node-dev
  • apollo-server, apollo-engine
  • Zeit Now

TypeScript

  • 型があったほうが書きやすい
  • 最近はクライアント(React)も全てTypeScriptで書いてるし...
  • apollo-serverのパッケージには型情報が含まれている

ts-nodeとts-node-dev

  • 本番はともかく、パフォーマンスを必要としないdevでいちいちコンパイルしてから実行するのはめんどくさい
  • ts-nodeを使えば.tsをそのまま実行できる
    • とある案件のdev環境でしばらく使っているが安定稼働している
  • ローカルで開発を行う際にはts-node-devを使う
    • --respawn オプション付きで起動するとファイルの更新を検知して再起動する

Apollo Server

apollographql/apollo-server: GraphQL server for Express, Connect, Hapi, Koa and more

  • GraphQL APIサーバを立てるのに必要なものがだいたい揃ってる全部入りパッケージ
  • とある案件ではgraphql-yogaというものを使っているが、これもapollo-serverとだいたい同じもの
    • apollo-serverのほうがメジャーなので近々乗り換えようと思っている

Apollo Engine

Apollo Engine | The GraphQL Gateway with essential features like caching and performance tracing.

  • apollo-serverに追加することでクエリのキャッシュやモニタリングが可能になる
  • ユニークなのはフィールドごとにリクエスト回数が可視化されるところ
    • People.bloodTypeは1回もリクエストされてないからdeprecatedにしよう...といった判断に使える
  • Free版はカスタムアラートが使えない。有償版は$99/mo~ とややお高め
  • 既存サーバへの組み込みも容易

ZEIT Now


続く

nomusiclife.hatenablog.jp

TypeScriptにおける、既存クラスの拡張

TypeScriptでAudioParam.cancelAndHoldAtTime()コンパイルを通したかったが、本家の型定義には追加されていないので、自分で拡張を行ったときのメモ。

declare global {
  interface AudioParam {
    cancelAndHoldAtTime(cancelTime: number): void
  }
}

Declaration Merging · TypeScript

compiler merges two separate declarations declared with the same name into a single definition.

自分でAudioParam interfaceを追加すると、元々存在するAudioParam interfaceにマージされる。

You can also add declarations to the global scope from inside a module:

AuidoParamがglobal scopeにいるのでGlobal augmentationを使う

参考