作ればわかる、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 を参照するようになり、動作しなくなる。

ちょっとしたappのビルドはParcelでいいかもしれない

Parcelというapplication bundlerが話題なので早速試してみた。
リリースから5日で7000以上のGitHub starを集めている。

Parcelとは

📦 Parcel

fast, zero configurationを謳うapplication bundler。
要するにconfigの要らない、ビルドの速いwebpackのようなもの。

ちょっとしたものを書くにも(boilerplateを流用できるとはいえ)configの管理は手間で、自分のようにちょっとしたアイデアを試すのにappを作るような場合には非常に助かる。

parcel-bundlerのインストール

$ yarn init -y
$ yarn add parcel-bundler --dev

エントリーポイントの作成

parcel-bundler/parcel: 📦🚀 Blazing fast, zero configuration web application bundler

Parcel can take any type of file as an entry point, but an HTML or JavaScript file is a good place to start. If you link your main JavaScript file in the HTML using a relative path, Parcel will also process it for you, and replace the reference with a URL to the output file.

htmlをエントリーポイントにすることで、パスの解決をいい感じに行ってくれる。

index.html

<html>
<body>
  <script src="./application.js"></script>
</body>
</html>

application.js

import { message } from './message'
document.body.innerText = `message: ${message}`

message.js

export const message = 'Hello, Parcel!'
$ yarn parcel index.html
yarn run v1.3.2
$ “/path_to_project/node_modules/.bin/parcel" index.html
Server running at http://localhost:1234
✨  Built in 160ms.

これだけである。
dev-serverもビルトインされているので http://localhost:1234 にアクセスして確認する。
ファイルの保存をすればauto reloadもちゃんと動作する。

※ ビルド済みのコードが dist/ 、キャッシュが .cache/ 以下に作成されるので .gitignore に追加しておきたい

React

「ちょっとしたapp」を作る場合、最近だとReactを使うことが多いので対応状況が気になる。

$ yarn add react react-dom
$ yarn add babel-preset-react babel-preset-env --dev

.babelrc

{
  "presets": ["env", "react"]
}

box.jsx

import React from 'react'

export default function Box(props) {
  return <h1>{props.message}</h1>
}

application.js

import React from 'react'
import { render } from 'react-dom'
import Box from './box'

render(
  <Box message="Hello React component!!" />,
  document.querySelector('#app')
)

index.html

<html>
<body>
  <div id="app">
  <script src="./application.js"></script>
</body>
</html>
$ yarn parcel index.html
yarn run v1.3.2
$ “/path_to_project/node_modules/.bin/parcel" index.html
Server running at http://localhost:1234
✨  Built in 1.48s.

たったこれだけ。
拡張子 .jsx も解決できている。
React込みのビルドをしているとは思えないほど速い。

所感

Zero configurationであるが故に、仕事で使っていると痒いところに手がとどかないということは当然出てくると思う。
しかし、休日にちょっとしたスケッチ気分でappを書きたいときには十分使えるなという印象。

もともとwebpackのビルド速度もそれほど遅いと感じたことはないし、趣味で作っているものなんてサイズはたかが知れているのだが、それでもビルドが速いというのは気持ちがいいものだ。

Google Apps Scriptを正確なスケジュールで実行する

課題

  • GASで「毎日 HH:MM に実行」といった感じのタスクスケジューリングをしたい
  • トリガー「時間主導型/日タイマー」だと「午前8時〜9時」のような、ざっくりとした指定しかできない
  • 「時間主導型/特定の日時」は YYYY-MM-DD HH:MM で指定ができ、指定した時間に正確に実行してくれるが、日時のピンポイント指定のみで条件指定ができないため、1実行につき1件のトリガーを作成しなければならず、運用するのは非現実的

ソリューション

「特定の日時」トリガーを動的に生成するタスクを「日タイマー」トリガーで実行する

サンプルコード

  • 下の例では createTriggers() で、土日祝を除く毎日、12:00、13:00、14:00に main() を実行するためのトリガーを生成する
  • 生成された main() のトリガーは実行が完了した後も残り続けるため、 ScriptApp.deleteTrigger() で掃除する
    • 削除する際に、 createTriggers() のトリガーを一緒に削除しないよう、trigger.getHandlerFunction() === 'main’ のものに絞って削除を行う
  • createTriggers() を実行するためのトリガーを「時間主導型/日タイマー/午前10時〜11時」で作成する
function isHoliday(date) {
  if (date.getDay() === 0 || date.getDay() === 6) {
    return true
  }
  
  // 日本の祝日カレンダーに終日予定があれば祝日とする
  var calendar = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com')
  var events = calendar.getEventsForDay(date)
  
  return events.length > 0
}

function createTriggers() {
  console.log('createTriggers')
  var now = new Date()

  // 残っているトリガーを掃除する
  var triggers = ScriptApp.getProjectTriggers()
  if (Array.isArray(triggers)) {
    triggers.forEach(function(trigger) {
      // mainのトリガーのみを削除する
      if(trigger.getHandlerFunction() === 'main') {
        ScriptApp.deleteTrigger(trigger);
      }
    })
  }
  
  // 土日祝はスケジュールしない
  if (isHoliday(now)) {
    console.log('there are no schedules today')
    return
  }
  var hours = [12, 13, 14]
  hours.forEach(function(hour) {
    var date = new Date()
    date.setHours(hour)
    date.setMinutes(0)
    if (now.valueOf() < date.valueOf()) {
      // main() のトリガーを指定した日時で作成
      ScriptApp.newTrigger("main").timeBased().at(date).create()
    }
  })
}

function main() {
  console.log('hello.')
}

実行結果

createTriggers() が日タイマーで実行された結果、11:00、12:00、13:00 のトリガーが生成されている。
(なぜか並び順は滅茶苦茶だが...)

f:id:nishaya:20171110152856p:plain

各トリガーの実行結果。
秒はまちまちだが、時/分までは指定した時刻に実行されているのがわかる。
(GASで console.log() した内容はStackdriver Loggingで確認できる)

f:id:nishaya:20171110152907p:plain

まとめ

  • GASを正確に定期実行したい場合、デフォルトで用意されている時間主導型トリガーのタイマーではなく、 ScriptApp.newTrigger() を使用する
  • 生成したトリガーは実行された後も残るので、次のトリガー生成のタイミングにリセットが必要

iPhone X ファーストインプレ

iPhone X、11/3(金)に入手することができた。
Apple表参道にピックアップに行ったが、3日の18時すぎだったので受け取りはスムーズに完了した。

忘れないうちにインプレを記しておく。

気づいた点

  • ホームボタンないのは1日で慣れる
    • 3日経つと「最初からホームボタンなどいらなかったのでは?」という気持ち
    • Control Center、右上から引っ張り出さないとならないのは少し遠く感じる
  • Face IDはコツさえつかめば十分Touch IDを置き換えられる
    • キッチンやお風呂で指が濡れてても使える
    • 真っ暗な場所でも動作する
    • ただし、横になっているときは失敗しやすい。要因の一つとして、横になっているときは顔と画面の距離が近い。気持ち遠くで構えることで成功率を高められる
    • Face ID認証後、ロック画面でスワイプしないとロック解除されないのが不便。通知を見逃さないためには仕方ないと思うが、Touch IDのときのように認証完了後ダイレクトにホーム画面に遷移するオプションがほしい
      • 対策としては、Face IDの認証が成功することを見込んで、スリープ解除したらすぐにスワイプしておく
  • OLEDはなんといっても黒
    • iPhone Xのために用意されたような真っ黒の壁紙がプリセットされている
    • OLEDでは黒 = 消灯になるため消費電力を抑える効果もある
      • Kindleなど、リーダー系のappは黒背景にしておくとバッテリ消費が抑えられそう
  • ディスプレイサイズ
    • 8 Plusに比べて広くなった感じはないが、縦の情報量が増えたのでスクロールを要する画面は快適になった
    • 上部の欠けは使ってるとそれほど気にならない
  • カメラ
    • iPhone 8 Plusからの変更なので正直あまり驚きはない
    • が、望遠側カメラで動画を撮影する際に光学手ぶれ補正が効くようになっている(8 Plusは広角側カメラのみ)
    • 撮影できる画像のアスペクト比は変わっていないので、撮影時の画面は上下に太いフチが付いてしまって寂しい。両脇削ってでも全画面で撮れるモードがほしい
  • ずっしり重い
    • 8 Plusよりは軽いが、比較的コンパクトなフットプリントのおかげかずっしり重く感じる
    • 4.7インチのiPhoneを使っていた人は重く感じるかも
  • ワイヤレス充電
    • オフィシャルのレザーケースを着けているが、PanasonicやAnkerのQi充電器で問題なく充電を行えた
  • シルバーを選んだが、クロムメッキされたステンレスフレームは初代〜3GSや初期のiPod Touchを思い起こさせる

まとめ

  • Plus系からのアップグレードだと、機能追加とコンパクトさを同時に手に入れられて満足度が高い。
  • 操作系に対する懸念はあったが、3日で慣れた。ただし、従来のものと全く別の操作を要求されることもある(Control Centerなど)ので、プライベートがX、仕事用は8などの複数台持ちをすると少しストレスがかかりそう
  • 黒が黒であることのありがたみ。テレビもそのうち有機ELにしたい

teenage engineering OP-1

f:id:nishaya:20170902081546j:plain

シーケンサを作っていた間は、影響をモロに受けてしまいそうなので我慢していたOP-1を、シーケンサの制作が一段落ついたこともあって買った。

触ってみるとシーケンサにあたるものはおまけ程度で、メインはシンセサイザー or ドラムキット(それに、FX/EQ)と、4トラックのレコーダー。

初見だと何をしていいのか戸惑うUIだが、OP-1の設計思想が頭に入ると腑に落ちる。
各インストゥルメント、エフェクトのビジュアライズが個別に用意されていて楽しい。そういうところに発揮できるユーモアと、それを実装できる余裕は持っていたいなと思う。

シャーシは切削されたアルミでそこそこ重量はあるので、見た目ほど持ち運びに適した感じではないのだけど、コンピュータの電源を入れずに音を作って遊べるので、旅行に持っていって移動中や宿でのんびりするときに弄るのがよさそうだ。

Reactでパターンシーケンサを作った

以前、シンセサイザーを作ったが、
楽器が弾けないのでブラウザに自動演奏してもらうことにした。

f:id:nishaya:20170827210726p:plain

Reaktion(DEMO)

Flowtype

シーケンスや音源のプリセットにflowtypeのアノテーションを使ったが、内部で生まれては消えるようなデータならわざわざclassを定義するまでもないし、Flowtypeなら必要な部分にだけ適用することができるので、導入したことで効率的に開発を進めることができた。

Material-UI

本当はGrommetのようなメリハリの効いたのが使いたかったんだけど、コンポーネントのバリエーションが足りなくて今回もMaterial-UIを使った。

とはいえ、Material-UIにも楽器を作る上で必要なコンポーネントが全て揃っているわけではないので、楽器のためのUIコンポーネントライブラリを作ってみるのもアリかもしれない。

音を鳴らすしくみ

肝心の音を鳴らすしくみについては
リファクタリングしながら書いていこうと思う。

追記: 2017/10/27
記事公開されました

tech.speee.jp