Edited at

3時間で自動作曲プログラムを作る

More than 1 year has passed since last update.

これは Nextremer Advent Calendar 2016 の3日目の記事です。

HTML5 には Web Audio API という音声を扱う強力な API があります。これを使って簡単な自動作曲プログラムを書きました。

Web Audio API をある程度知っている人向けの記事です。

デモは Chrome で開くことを推奨します。


とにかく音を鳴らしてみる

こんな HTML を書きました。play を押すと音がなるだけのものです。

Gain を2つ噛ましてますが、masterGain は全体の音量を調節し、gain は音毎の音量を調節するという役割です。

masterGain は経験上、 0.1 とか小さめにしないとうるさい。

<!DOCTYPE html>

<html lang="ja">
<head>
</head>
<body>
<button onclick="play()"> play</button>
<script>
const AudioContext = window.AudioContext;
const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

function play() {
const timeNow = ac.currentTime;
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="square";
osc.connect(gain);
gain.connect(masterGain);
osc.start(timeNow);
osc.stop(timeNow + 0.1);
}
</script>
</body>
</html>

デモ1


和音を鳴らす

chord 関数で和音を鳴らせるようにしてみました。

note1 は単体の音を鳴らす関数です。任意の音高で鳴らせます。MIDI に倣い、 nn = 60 がドに当たります。

和音は、ルート音(root)と、コードのタイプ(type)から作られます。type はとりあえず長三和音(major)と短三和音(minor)を用意しました。

playを押すと C-F-G とコード進行が鳴ります。

(なんか初っ端の和音、一音しか鳴らないときがある気がするぞ・・・)

const AudioContext = window.AudioContext;

const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

function play() {
const timeNow = ac.currentTime;
chord(timeNow, 60, 'major', 1);
chord(timeNow + 1, 65, 'major', 1);
chord(timeNow + 2, 67, 'major', 1);
}

function note1(time, nn, dur) {
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="square";
const freq = 440 * Math.pow(2, (nn - 69) / 12);
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function chord(time, root, type, dur) {
const ds = {
'major': [0, 4, 7],
'minor': [0, 3, 7],
}[type];

for (const d of ds)
note1(time, root + d, dur);
}

デモ2


無限に演奏する

setInterval を使って無限に演奏するようにしました。

js のジェネレータがいい感じにハマってると思う。

C-F-G-C-C-F-G-C-... と、コード進行を延々に繰り返します。

const AudioContext = window.AudioContext;

const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

const barLength = 2; // 一小節当たりの秒数

function play() {
let timeNext = ac.currentTime + 0.01;
let i = 0;

function* nextBar() {
while (1) {
chord(timeNext, [60, 65, 67, 60][i], 'major', barLength);
i = (i+1)%4;
yield;
}
}

const barGen = nextBar();
setInterval(() => {
if (timeNext - barLength < ac.currentTime) {
barGen.next();
timeNext += barLength;
}
}, 500);
}

function note1(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="square";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function chord(time, root, type, dur) {
const ds = {
'major': [0, 4, 7],
'minor': [0, 3, 7],
}[type];

for (const d of ds)
note1(time, root + d, dur);
}

デモ3


バスドラを追加

リズムが無いと寂しいのでバスドラを足します。

exponentialRampToValueAtTime で周波数を下げるように三角波を鳴らすことで、電子的なバスドラの音を再現します。

スネアも欲しかったのですが、簡単に音を作る方法が思いつかなかったです。

(バスドラの音、聞こえますかね?)

const AudioContext = window.AudioContext;

const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

const barLength = 2; // 一小節当たりの秒数

function play() {
let timeNext = ac.currentTime + 0.5;
let i = 0;

function* nextBar() {
while (1) {
const time = timeNext;

// コード
chord(time, [60, 65, 67, 60][i], 'major', barLength);

// ドラム
bassDrum(time + barLength * 0);
bassDrum(time + barLength * 0.25);
bassDrum(time + barLength * 0.5);
bassDrum(time + barLength * 0.75);

i = (i+1)%4;
yield;
}
}

const barGen = nextBar();
setInterval(() => {
if (timeNext - barLength < ac.currentTime) {
barGen.next();
timeNext += barLength;
}
}, 500);
}

function note1(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="square";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function chord(time, root, type, dur) {
const ds = {
'major': [0, 4, 7],
'minor': [0, 3, 7],
}[type];

for (const d of ds)
note1(time, root + d, dur);
}

function bassDrum(time) {
const dur = 0.2;
const gain = ac.createGain();
gain.gain.setValueAtTime(2, time);
gain.gain.setValueAtTime(2, time + dur * 0.5);
gain.gain.exponentialRampToValueAtTime(0.1, time + dur);
const osc = ac.createOscillator();
osc.type="triangle";
osc.frequency.setValueAtTime(300, time);
osc.frequency.exponentialRampToValueAtTime(20, time + dur);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

デモ4


メロディを追加

メロディの(ようなもの)を足してみました。

一拍ごとに、和音の構成音をランダムに一つ鳴らすだけという実装です。

(曲っぽくなってる?)

const AudioContext = window.AudioContext;

const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

const barLength = 2; // 一小節当たりの秒数
const beatLength = barLength / 4; // 一拍当たりの秒数

function play() {
let time = ac.currentTime + 0.5;

function* nextBar() {
let i = 0;

while (1) {
const root = [60, 65, 67, 60][i];
const type = 'major';

// メロディ
for (let j = 0; j < 4; ++j)
note2(time + beatLength * j, randGet(chordNotes(root, type)) + 12, beatLength);

// コード
chord(time, root, type, barLength);

// ドラム
bassDrum(time + barLength * 0);
bassDrum(time + barLength * 0.25);
bassDrum(time + barLength * 0.5);
bassDrum(time + barLength * 0.75);

i = (i+1)%4;
yield;
}
}

const barGen = nextBar();
setInterval(() => {
if (time - barLength < ac.currentTime) {
barGen.next();
time += barLength;
}
}, 500);
}

function note1(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 0.5;
const osc = ac.createOscillator();
osc.type="square";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function note2(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="sawtooth";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function chordNotes(root, type) {
const ds = {
'major': [0, 4, 7],
'minor': [0, 3, 7],
}[type];
return ds.map((x) => x + root);
}

function chord(time, root, type, dur) {
for (const nn of chordNotes(root, type))
note1(time, nn, dur);
}

function bassDrum(time) {
const dur = 0.2;
const gain = ac.createGain();
gain.gain.setValueAtTime(2, time);
gain.gain.setValueAtTime(2, time + dur * 0.5);
gain.gain.exponentialRampToValueAtTime(0.1, time + dur);
const osc = ac.createOscillator();
osc.type="triangle";
osc.frequency.setValueAtTime(300, time);
osc.frequency.exponentialRampToValueAtTime(20, time + dur);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function randGet(arr) {
return arr[Math.random() * arr.length | 0];
}

デモ5


コード進行にバリエーションをもたせる

http://supuhuri.sub.jp/tukurikata1.htm

このページを参考に5つほどコード進行を用意し、ランダムに選択して演奏するようにしました。

ついでにリファクタリングしました。

メロディの音が高くなりすぎないように音域を制限しようとしましたが、聴いてみるとしないほうがいいなと思ってコメントアウトしてあります。

const AudioContext = window.AudioContext;

const ac = new AudioContext();

const masterGain = ac.createGain();
masterGain.gain.value = 0.1;
masterGain.connect(ac.destination);

const barLength = 2; // 一小節当たりの秒数
const beatLength = barLength / 4; // 一拍当たりの秒数

function play() {
const barGen = nextBar(ac.currentTime + 0.5);
let time = barGen.next().value;

setInterval(() => {
if (time - barLength < ac.currentTime) {
time = barGen.next().value;
}
}, 500);
}

function* nextBar(time) {
const chordGen = nextChord();

while (1) {
const {root, type} = chordGen.next().value;

// メロディ
for (let j = 0; j < 4; ++j)
note2(time + beatLength * j,
randGet(chordNotes(root, type)) + 12,
//clip(randGet(chordNotes(root, type)) + 12, 72, 84),
beatLength);

// コード
chord(time, root, type, barLength);

// ドラム
for (let i = 0; i < 4; ++i)
bassDrum(time + beatLength * i);

time += barLength;
yield time;
}
}

function* nextChord() {
const rootTable = {
C: 60, D: 62, E: 64, F: 65, G: 67, A: 69, B: 71
};
const chordProgs = [
'Cmajor Eminor7 Fmajor G7',
'Fmajor G7 Eminor7 Aminor',
'Aminor Fmajor Gmajor Cmajor',
'Fmajor Eminor7 Dminor7 Cmajor',
'Cmajor Gmajor Aminor Eminor Fmajor Eminor Fmajor Gmajor',
].map(x => x.split(' ').map(y => {
return {
root: rootTable[y[0]],
type: y.substr(1)
};
}));

while (1)
yield* randGet(chordProgs);
}

function note1(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 0.5;
const osc = ac.createOscillator();
osc.type="square";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function note2(time, nn, dur) {
const freq = 440 * Math.pow(2, (nn - 69) / 12);
const gain = ac.createGain();
gain.gain.value = 1;
const osc = ac.createOscillator();
osc.type="sawtooth";
osc.frequency.setValueAtTime(freq, time);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function chordNotes(root, type) {
const ds = {
'major': [0, 4, 7],
'minor': [0, 3, 7],
'7': [0, 4, 7, 10],
'minor7': [0, 3, 7, 10],
}[type];
return ds.map((x) => x + root);
}

function chord(time, root, type, dur) {
for (const nn of chordNotes(root, type))
note1(time, nn, dur);
}

function bassDrum(time) {
const dur = 0.2;
const gain = ac.createGain();
gain.gain.setValueAtTime(2, time);
gain.gain.setValueAtTime(2, time + dur * 0.5);
gain.gain.exponentialRampToValueAtTime(0.1, time + dur);
const osc = ac.createOscillator();
osc.type="triangle";
osc.frequency.setValueAtTime(300, time);
osc.frequency.exponentialRampToValueAtTime(20, time + dur);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + dur);
}

function randGet(arr) {
return arr[Math.random() * arr.length | 0];
}

function clip(nn, lower, upper) {
while (nn < lower)
nn += 12;
while (upper < nn)
nn -= 12;
return nn;
}

デモ6


おわり

いかがでしたでしょうか。

個人的には延々流してられるような曲が生成できていると思います。あとプログラム自体書いてて楽しかったです。

果たしてこれを自動作曲とよんでいいのかわかりませんが皆さんもぜひ自動作曲に挑戦してみてください。


やり残したこと

やりたかったけど、時間がなくて断念したことを書き留めておきます。。。


  • 周波数変調

  • もうちょっとまともなメロディ生成(n分音符, 休符, シンコペーション, 非和声音, etc)

  • マルコフ連鎖によるコード進行生成

  • アルペジオの生成

  • ベースやスネアなどのドラム隊の実装