オンライン同期可能なシーケンサーを作った話 前編
少し前になるが、また音の出るおもちゃを作ったので記録を残す。
長くなりそうなので前編/後編に分ける。
作ったもの
- ブラウザで動くマルチトラックシーケンサ
- 作成中の楽曲をオンライン保存できる
- URLシェアにより、別のユーザと楽曲の同時編集ができる
- 編集内容は複数クライアント間でリアルタイム同期される
使ったもの
- React/Redux
- Firebase
- Cloud Firestore
- Authentication
- Hosting
- TypeScript
- Web Audio API
発表資料
モチベーションと課題
Reactで音の出るおもちゃを作るのが好きなので、Reactでシンセサイザーを作ったり、Reactでパターンシーケンサを作ったりしてきた。
今まで作ってきたものは音を出したり、パターンの変化を楽しんだりとどちらかといえば即興性の高いものだったが、「曲を作る」となると保存しておき続きを作り進められることや、他者と協力して作曲ができることが求められるだろうと考え、その検証として今回はオンライン保存、同期編集が可能なシーケンサを作るに至った。
- ブラウザ上で音を鳴らせる
- タイミング通りにシーケンス(作成した曲データ)を鳴らせる
という要件については今までの成果物で検証できていたので、今回は
- ブラウザ上でシーケンスをグラフィカルに編集できる
- 編集したものをオンライン保存できる
- 保存したものをシェアして複数のクライアントから同時編集できる
といった点にフォーカスを当てて実装を行った。
完成したものについてはデモを触ってみていただきたいが、実装を進める中で発見のあった箇所について書きたいと思う。
ピアノロールの実装
シーケンス編集の手段としては、一般的なDAW(音楽制作ソフトウェア)なら大抵備えているピアノロールを採用した。
ピアノロールはx軸で時間、y軸で音階を表現するUIだ。
探してみると、Reactでピアノロールを実装している例はいくつかあったものの、そのまま利用できそうなものがない、理想とするピアノロールと違うといった理由から、自前で実装することにした。
ちなみに、私が理想とするピアノロールは、かつて存在したOpcode社のEZ Visionというシーケンサのものなので、今回作ったものもそれに似た挙動になっている。
SVG as JSX
ピアノロールの実装はSVGをReact Componentに内包する形で実装した。
SVGのタグをJSXの中で使うことで、以下のようなメリットが得られた。
- 図形のグループをReact Componentに切り出しやすい
- グリッドのbackground patternも別コンポーネントに切り出した
- 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については後編で書く
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 モニタリング編
前回の続き。
作成した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" の項目を見ると実行したクエリのログが確認できる。
- リクエストの頻度
- パフォーマンスの概要
- エラー率
クエリ名ごとにさらに詳細な情報を確認することもできる。
Daily Report
Slack integrationを設定しておくとdaily reportでモニタリングのサマリを受け取ることもできる。
Schema Explorer
Schema Explorerではschemaに含まれるtypeの一覧と、それぞれの使用状況を確認することができる。
例えば、これを見るとPerson.addressは全く使われていないことがわかるので @deprecated
にする、といった判断に使える。
Schema History
schemaの変遷を確認できる。
例えば、Person.addressを @deprecated
に変更した場合、以下のように表示される。
まとめ
- 小一時間くらいあればここまで準備できる
- あとはSchema足していってResolverをひたすら書く
- とりあえずクライアントの動作を確認するためだけのモックサーバならmock書いていくだけでもOK
- GraphQLのサーバ運用するのはそれなりに労力かかる(と聞く)が、モニタリングあればただ闇雲にやるよりはずっと安心
Apollo ServerとNowで作るGraphQLモックサーバ #3 デプロイ編
前回の続き
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の設定にしたがってデプロイが始まる。
デプロイが完了すると下記のようなメッセージが表示され、 commit messageの横にある✓のアイコンからデプロイされた環境にアクセスできるようになる。
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でアクセスできるようになる。
モニタリング編に続く
Apollo ServerとNowで作るGraphQLモックサーバ #2 実装編
モックサーバの実装
前回
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のいいところ。
一応値は返ってきているが、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]が返る }), }
それっぽい値が返ってくるようになった
デプロイ編に続く
Apollo ServerとNowで作るGraphQLモックサーバ #1 準備編
GraphQLのモックサーバを作る際に使うものをメモ
使うもの
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
続く
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を使う