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を使う

参考

create-react-appで絶対パスによるimportをする

src/Components/App.jsxsrc/index.js からimportする場合、create-react-appでは

import App from './Components/App';

と書くが、これを、

import App from 'Components/App';

こう書きたい。
が、デフォルトではパスの解決ができず、 Module not found: Can't resolve 'Components/App’ エラーとなる。

NODE_PATHを設定する

プロジェクトの .envNODE_PATH=src を設定することで解決する。
※ dev serverの再起動が必要

NODE_PATH=src

Advanced Configuration

Same as NODE_PATH in Node.js, but only relative folders are allowed. Can be handy for emulating a monorepo setup by setting NODE_PATH=src.

余談だが、create-react-appのカスタム環境変数は、上記のAdvanced Configurationに含まれるものと、REACT_APP_ プレフィクスを持つもの、それに NODE_ENV のみが有効となる。

Adding Custom Environment Variables

You must create custom environment variables beginning with REACT_APP_. Any other variables except NODE_ENV will be ignored to avoid accidentally exposing a private key on the machine that could have the same name.

TypeScriptによるStateless Functional Componentの定義

環境構築は create-react-appMicrosoft/TypeScript-React-Starterでサクッと行う

$ npx create-react-app my-app --scripts-version=react-scripts-ts
$ cd my-app
$ npm run start

以前、TypeScriptでReact app作ろうとしてセットアップに手間取ったときとは隔世の感がある。

type React.SFCを使う

@types/reactReact.SFC という型が用意されているので、これを使う

DefinitelyTyped/index.d.ts at master · DefinitelyTyped/DefinitelyTyped

    type SFC<P = {}> = StatelessComponent<P>;
    interface StatelessComponent<P = {}> {
        (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
        propTypes?: ValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }

type SFC<P = {}> とあるので、全てのpropsはoptionalにする必要がある。
そのため、requiredで定義すると以下のコンパイルエラーが発生する。

 error TS2322: Type '{}' is not assignable to type 'IntrinsicAttributes & AppProps & { children?: ReactNode; }'.
  Type '{}' is not assignable to type 'AppProps'.
    Property 'name' is missing in type '{}'.

コンポーネントの書き換え

App.tsxをStateless Functional Componentに書き換えるとこのようになる

import * as React from 'react';
import './App.css';

const logo = require('./logo.svg');

interface AppProps {
  name?: string;
}

const App: React.SFC<AppProps> = ({ name }) => {
  return (
    <div className="App">
      <div className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h2>Welcome to {name}</h2>
      </div>
      <p className="App-intro">
        To get started, edit <code>src/App.tsx</code> and save to reload.
      </p>
    </div>
  );
};

App.defaultProps = {
  name: 'React.SFC',
};

export default App;

defaultPropsの定義をコンポーネント定義と分けたくない場合は、

const App: React.SFC<AppProps> = ({ name = 'React.SFC' }) => {
  return (
    <div className="App">
...

とも書ける

React Sketch.app

フロントエンドのデザインと実装のワークフローをスムーズにできないか、というのを考えているときにReact Sketch.appというそれっぽい名前のツールを見つけた。

React Sketch.appとは

そもそもSketch使ったこともなかった(UIに特化したIllustratorくらいの認識)ので、チュートリアルを触りながらどんなものか確認してみた。

Introduction · react-sketchapp

jsxのコードを変更して保存するとSketchにリアルタイム反映される。
これだけだとdev-serverで開発してるのとさして変わらんという印象を受けたので、どういうケースにハマるのかドキュメントを読んでみる。

Managing the assets of design systems in Sketch is complex, error-prone and time consuming. Sketch is scriptable, but the API often changes. React provides the perfect wrapper to build reusable documents in a way already familiar to JavaScript developers.

Sketchでデザインシステムのアセットを管理する手間を軽減する。 SketchのAPIがころころ変わるので、Reactでラッパーを作ったと。

できること

  • デザインシステムを実コード(React components)で実装できる
  • React Componentの実物を使って画面デザインができる
  • 実データを反映したデザイン成果物を作れる

つまづきポイント

チュートリアルのコードを動かしている最中、突然コケるようになった

  • saveするたびにPageが作られる
  • TypeError: MSAttributedString.alloc().initWithAttributedString is not a function. というエラーを吐いてコードをSketchに反映しなくなる

踏んでいたのは以下のバグ

github.com

最新のSketch(48.2)を使っていたのが原因。47を使えとのこと
チームで使う場合は事前にSketchのバージョン合わせておく必要がありそう

まとめ

エンジニア主導の現場かつ、大きめのデザインシステムを導入しているプロジェクトではワークフローをシンプルにできそう。

一方で、JSX(React Sketch.appのAPIも加わるのでさらに難易度上がる)をデザイナーに書いてもらうのは現実的なのか、という課題は残る。
また、Sketchのバージョンアップに伴ってAPIが変更された場合に動作しなくなるケースがあるため、Sketchのバージョンを少し古いものに揃えなければならない。

renderSomething() メソッドを潰す

年末だし大掃除しよう、
ということでClass Componentにある render* というメソッドを潰している。

stateless functionへの書き換え

具体的には、こんな感じになっているのを

class Foo extends React.Component {
  renderSomething() {
    return <div>something</div>
  }

  render() {
    return <div>{this.renderSomething()}</div>
  }
}

こうしたい

const Something = () => <div>something</div>
const Foo = () => (
  <div>
    <Something />
  </div>
)

eslintにruleを追加する

都度search and destroyするのは効率が悪いので
eslintのruleを追加して引っかけることにした。

.eslintrc に以下の設定を追加。

rules:
  no-restricted-syntax:
    - warn
    - selector: "MethodDefinition[key.name=/^render.+/]"
      message: "Don't use render* method"

プロジェクトでは error でCIをコケさせてマージを禁止しており、 本件はMUSTではないため warn にとどめている。

作ればわかる、FM音源

この記事はSpeee Advent Calendar 2017 15日目の記事です。

前日は@cho3による「サンタクロースの正体を暴く」でした。

動機

昔からFM音源の音は好きだったが、どういう原理で音作りをしているのか詳しくは知らなかった。

FM音源がどんな音色を出すかについては、ここに貼った動画を見てほしい。

www.youtube.com

一言で言うと80年代の音。
とはいえ90年代以降も多用されていて、メガドライブの内蔵音源や、ガラケーの着メロを鳴らしていたのもFM音源だ。

とある記事(アナログシンセとはまったく違う、“誰でもわかるFM音源”講座 : 藤本健の“DTMステーション”)を読んでいたら、仕組みについてはなんとなく理解できたので、実際に動くものをWebAudioとReactで作ってみることにした。

そうFM音源とは周波数変調をかける音源であり、OP2でOP1にビブラートをかけるというか、くすぐるんです。OP1はもともとポーっと鳴っていた音が、くすぐったいのでピーとかジーとがギャーって鳴るんです

本稿では一般的なFM音源の原理については詳しく触れないので、気になる方は記事を読んでみてほしい。

記事中に出てくるReface DXのUIはグラフィカルでFM音源の音作りも直感的にできそうだ(欲しい)。
以前、DX7の実機でパラメータをいじったときは全然理解できなかった。

オペレータを作る

FM音源において音を構成する最小要素、オペレータから作っていく。
OscillatorNodeとGainNodeをまとめたものにEG(ADSR)を追加してOperator classを作った。

f:id:nishaya:20171214181637p:plain

ADSRは数値だけだと直感的にエディットしづらいので可視化した。
SVGを使ったのは、DOMで記述できReactとの相性が良いため。

オペレータのルーティングをする

Operatorには変調をかけられる側(キャリア)とかける側(モジュレータ)があり、それぞれノードを接続する先が違う。
キャリアは音を出すので出力に直結するGainNodeに、モジュレータは変調をかけるのでキャリアのOscillatorNode(のfrequency)に接続する。

例えば、後述するElectric Pianoのルーティングは以下のようになる。

f:id:nishaya:20171214165523p:plain

※ nodeの可視化にはFirefoxWeb Audio エディターを使った

勘のいい読者ならお気づきと思うが、このルーティングはFM音源シンセサイザーにおけるアルゴリズムとほぼ同じだ。

音(ノート)を鳴らす

キーを押すとOscillatorNodeの発振が開始されて音が出る。 今回作ったFM音源は4OPなので1音発音するのに4つのOscillatorNodeのstart()を呼ぶ。

このとき、各オペレータに内包されているGainNodeに対し、ADSRのパラメータによるレベルの時間的変化をスケジュールする。 これによって、ゆっくり立ち上がる(長いattack)音や、鋭さを伴った(短いdecayと低いsustain)変調、キーを話したときの余韻(長いsustain)といった表現が可能になる。

WebAudio APIのOscillatorNodeは1回鳴らすごとに使い捨てなので、発音のたびに4つのOperatorを内包するNoteオブジェクトを生成し、アルゴリズムに沿ってNodeの接続をした後にOscillatorNodeの再生を行っている。

音色を作る

FM音源の音色作りは変調による波形の変化が極端なため、一般的には難解とされるが「くすぐり」を意識しながら作っていくとそれらしい音ができあがる。

1. Electric Piano

いかにもFM音源といった感じのエレクトリックピアノ
バリエーションとして 4. Toy Piano も作った

  • carrier #0 に 減衰の速い(decayを短めにした) modulator #1 をかけて「コツン」という感じの立ち上がりを与える
  • 音に厚みを出すため、デチューンした carrier #2 を用意し、金属的な響きを加えるためにfreq. ratioを大きめにとった modulator #3 をかける

2. Organ

オーソドックスなオルガン。

  • carrier #0 ~ #3を並列に並べ、それぞれを別のfreq. ratioで鳴らす

3. Synth Bass

クセの強いシンセベース。

  • carrier #0 に modulator #1~3 をかける。modulatorごとに異なるADSRを設定し、音色に時間的な変化を与えている

別途LPFをかけるとそれらしくなりそう

DEMO

My first FM synth

f:id:nishaya:20171214193716p:plain

※ A~Lのキーを鍵盤に見立てて演奏ができます

Chrome(63以降)推奨。 FirefoxだとAudioParam.cancelAndHoldAtTime()が使えないのでリリースが不自然になる問題が残っている。

Source Code

今回のdemoのビルドにはParcelを使った。
production buildするときにこのバグを踏んだのだけれど、翌日にリリースされた1.2.0で修正されていた。

Unable to `build` index.html · Issue #8 · parcel-bundler/parcel

TODO

  • とりあえず音が出るよう作ったのでリソースの管理が雑なのをなんとかする
  • オペレータにフィードバックを追加する
  • フィルタを追加する
  • アルゴリズムをグラフで可視化する

所感

難解だと思っていたFM音源も、自分で作ることによって理解が進み、ある程度までは思い通りの音を作れるようになった。

D-50のLA音源や物理モデル音源など、原理が気になる音源方式は無数にあるので、機会があればそれらも挑戦してみたい。


明日は@yoppiによる「GoとECSの格闘技について」 お楽しみに!

Parcelで絶対パスを使う場合はbabel-plugin-module-resolverを使う

問題

  • Parcelのdocsには相対パスによるimportのサンプルしか載っていない
  • import Box from ‘../../components/box’ とか書きたくない

babel-plugin-module-resolverでrootを解決する

本家のIssueより、
babel-plugin-module-resolver を使う。

Alias definition · Issue #25 · parcel-bundler/parcel

babel-plugin-module-resolver をインストールして、pluginの設定を追加する。

.babelrc

{
  "plugins": [
    ["module-resolver", {
      "root": ["./src"]
    }]
  ]
}

これでimportをsrc以下の絶対パスで書けるようになる。

例えば、 /src/components/box.jsx をimportするには、

import Box from ‘components/box’

となる。

注意点

パスに一致する名前を持つnpm package(上の例ではcomponents)がインストールされていると、node_modules/components/box を参照するようになり、動作しなくなる。