ブラウザでメロディーを鳴らす 【JavaScript】

JavaScript/HTML

JavaScript を使って、ブラウザでメロディーを鳴らす方法についてまとめておきます。
以下の環境で動作確認をしています。
環境: Windows パソコン、Microsoft Edge ブラウザ

背景 ~ ブラウザでメロディーを鳴らしてみる

以前に、ブラウザ上でドラムを鳴らす HTML のスクリプトについてまとめています。
そこで今回は、ブラウザ上でメロディーを鳴らすスクリプトについてまとめておきます。

ブラウザ上のテキスト欄に、音の長さと音階を並べておくと、任意のメロディーを再生できるようにします。
簡単な作曲程度であればブラウザ上でできるようになると思います。

C4、A4 などの国際式の表記で音階を書いておくと、MIDI のノートナンバーや周波数に換算します。Webサイトやプログラミングで音を扱う際に出てくる主要な概念を扱えるようにしておきます。
また、音階や和音を扱う際の計算式、物理・数学的な背景についても集約しておきます。

設定手順

① パソコン内にフォルダ(例:”desktop_music1″)を作成します。
例: c:\user\desktop_music1
② ①のフォルダの中に melody_player1.html、play_pattern1.js という名前でテキストファイルを作成し、下記のスクリプトを貼りつけて保存してください。
例:
c:\user\desktop_music1\melody_player1.html
c:\user\desktop_music1\play_pattern1.js
③ ②の melody_player1.html をダブルクリックしてください。

→ プレーヤーのスクリプトが起動したら、設定&動作確認まで完了です。

使い方

動くサンプル

まずは、動くサンプルを以下に貼っておきます。

※ 操作時に音が出ますので、事前に音量の調整をしておいてください。

操作画面について

・ [Scientific Pitch:] の欄に音階等を記載し、[play] ボタンをクリックするとメロディーを再生します。
・ [stop] ボタンで再生途中で停止できます。
・ [volume] 欄で音量の設定が可能です。
・ [BPM] (beats per minute)欄でテンポの設定が可能です。
・ [waveform] 欄で再生する音の種類(波形)を選択可能です。
ブラウザの仕様から、4種類(sine、square、sawtooth、triangle)からの選択としています。
・ [damping] で再生する音の減衰の設定が可能です。

音階の書き方

一例として、[Scientific Pitch:] 欄に以下のテキストを貼りつけて [play] ボタンをクリックしてください。

1.00 C4
1.00 D4
1.00 E4
1.00 F4
1.00 G4
1.00 A4
1.00 B4
1.00 C5

※ テキストの1行が、1つの音(音階)、または、コマンドに対応します。
※ 音を生成する場合は、各行に、「音の長さ」(音の拍数) + ” ” (半角スペース)+ 「音名」 の形式で記載していきます。
※ 「音の長さ」(音の拍数)は、半角の数値で記載します。1拍に対する音の相対的な長さで定義します。
たとえば、1.00 と記載すると、1拍分の長さになります。2.00 と記載すると、2拍分の長さになります。
※ 「音名」部分は、国際的表記(C4 など)で記載します。
※ C4 がピアノの中央のド(middle C)に対応します。A4 が国際標準音のラ(440Hz)に対応します。
※ 「音名」(国際的表記)について、末尾の1文字はオクターブ番号に対応させるようにします。
たとえば、C4 を1オクターブ下げるには C3、1オクターブ上げるには C5 とします。
※ シャープ、フラットとする場合は、「音名」部分の2文字目に、”s”、”f” を入れます。
たとえば、Cs4 は、ドのシャープ(C♯4)に対応します。

休符の書き方

休符を入れる場合は、拍数(時間)の部分のみを記載します。

1.00 

2.00、1.00、0.50、0.25 とすると、二分休符、四分休符、八分休符、十六分休符のように、休符の長さを任意に設定できます。

テンポ/BPMの設定

テンポを設定するには、たとえば以下を記載します。

set_BPM1 60

この場合、この行以降の音を1分間に60拍のテンポで再生します。(BPM = beats per minute)
“set_BPM1 120” とすると、120拍/分のテンポで再生します。

作成したメロディーの保存

[Scientific Pitch:] 欄で曲を作成して [play] で確認したら、[download] ボタンをクリックすることで、同欄の内容をダウンロードできます。
ダウンロードしたファイル(play_pattern1.js)は、①のフォルダに保存することで、上記の HTML を再表示させたときの初期値として設定できます。あるいは、①のフォルダ内の play_pattern1.js をテキストエディタなどで直接編集しても起動時の設定をすることができます。

うまく動いたら

・ うまく動いたら、何か曲を作ってみてください。Microsoft の Copilot や ChatGPT に頼めば、いくらでも曲の元ネタを作ってもらえると思います。元ネタに何度も修正を加えて、元の音階やリズムが完全に残らない程度に改変すれば、それはそれで独自のものと考えられるかなと思います。
・ また、プログラミングが得意な方は、スクリプトを自由に改変してみてください。
・ 国際的表記でメロディーのデータを作っておくと、MIDI のノートナンバーや周波数に変換できますので、いろいろなハードウェアやソフトウェアで活用することが可能になると思います。

ソフトウェアの動きについて
・ ソフトウェアの動きとしては、ユーザーが [Scientific Pitch:] 欄を編集して欄からマウスが離れると(欄外をクリックすると)、記載された内容から、MIDI ノートナンバーと、周波数を順次計算するようにしています。
・ また、ブラウザの内部仕様では周波数で音を設定するようになっています。そこで、プログラム内では、[play] ボタンをクリックしたとき、[Frequency notation:] に書かれた周波数で音を再生するようにしています。
・ したがって、もしユーザーが [Frequency notation:] の欄の周波数などを直接編集して [play] ボタンをクリックした場合には、この Frequency の欄に記載された周波数で音を出すようにしています。これにより、ピアノなどの12段階の平均律に縛られずに、任意の周波数の音を再生できるようにしています。
たとえば、440 Hz の音と 450 Hz の音を再生して、10Hz の差を聴き分けられるかといった音感のテストをすることも可能です。(私がやってみた限りでは、10Hz の聴き分けは可、5Hz (+1%) の聴き分けとなると厳しくなってきます。周波数帯にもよります。)
・ [MIDI notation: ] の欄も同様です。MIDI ノートナンバーを使って同欄を直接編集すると、
[play] ボタンで再生することが可能です。周波数を計算し、[Frequency notation: ] の欄を更新するようにしています。一例として、以下を [MIDI notation: ] の欄に貼りつけて、[play] ボタンをクリックしてみてください。

MIDI ノートナンバーで記載した例(月の光、Claude Debussy)


set_waveform1 sine

0.50 
0.50 68 
2.00 80 
1.50 77 

0.50 69 
0.50 75 
0.50 77 
3.00 75 

0.50 69 
0.50 73 
0.50 75 
0.50 73 
1.50 77 
1.00 73 

0.50 66 
0.50 72 
0.50 73 
3.00 72 

※ メロディーは、ざっと入力してみた程度のものです。音楽、プログラミングに興味のある方は、メロディーを追加・修正したり、スクリプトを自由に改善してみてください。

余談・その他

音楽関連の計算式のまとめ

音楽を扱うにあたり、オクターブや平均律などの概念が出てきますので、計算式の整理をしておきます。

ある任意の音階を整数 n を使って等間隔に定義することにします。整数 n に対応する音の周波数を f(n) とおくことにします。
任意の音階 n に対し、音階が a 段階だけ上がると、周波数 f(n) が2倍になる(1オクターブ上がる)ことが経験的にわかっているものとします。
すると、つぎの式が成り立ちます。

$$ f(n+a) = 2・f(n) … (*01)$$

ここで、あらたに任意の整数 k を定義し、f(n) の n を a・k に置きかえると、(*01) の性質を使って、以下の変形ができます。

$$ f(k・a) = 2・f((k-1)・a) = 2^{2}・f((k-2)・a) = … = 2^{k} f(0) … (*02)$$

関数 f(n) を連続関数とみなし、改めて k・a = n、k= n/a と置きなおし、k を消去するとつぎのようになります。

$$ f(n) = 2^{n/a} f(0) … (*03)$$

少し変形し、両辺を \( \log_{2} \) に入れると累乗の部分を係数として出すことができます。

$$ \frac{f(n)}{f(0)} = 2^{n/a} … (*04)$$
$$ \log_{2}{\frac{f(n)}{f(0)}} = \frac{n}{a} … (*05)$$

何がしか音階というものがあったとして、音階を均等の幅(整数)となるよう割りつけたい。そして、音階 n が a 段階上がると周波数が2倍になる(1オクターブ)といった性質があるとすると、上記のように log の関数が自然に導かれることになります。右辺を見ると、1オクターブの段階 a に対し、音階 n が変化すると均等に変化するように定義できています。(音階というものがどうなっているのか調べようとすると、数学が出てくる。。)

また、(*03) より、以下が得られます。

$$ \frac{f(n+1)}{f(n)} = \frac{2^{(n+1)/a}f(0)}{2^{n/a}f(0)}=2^{1/a} … (*06)$$

任意の音階 n について、音階が1つ上がると周波数は \(2^{1/a}\) 倍になることになります。

12平均率の導入

ここで、a = 12 として1オクターブを12段階で分割するものとします。また、基準音として f(0) = 440 Hz とします。すると、以下となります。

$$ f(n) = 440・2^{n/12}, Hz … (*07)$$

このとき、(*06) より、以下が得られます。

$$ \frac{f(n+1)}{f(n)} =2^{1/12} … (*08)$$

つまり、音階が1つ上がると、周波数は \(2^{1/12}\) (=1.059..)倍になります。周波数が6%上下すると隣の音階になるということになります。

このときの 440Hz の周波数を基準の音階(ラ、A)とし、音階がひとつ上がると周波数が \(2^{1/12}\) 倍の関係となるよう、12段階の音階(A ~ G)を割りつけていくものとします。

オクターブ番号について 

また、オクターブ番号については、ピアノなどの楽器等が生成できる最低音域がオクターブ番号0となるよう定義します。
たとえば、88鍵のピアノは 7.3 (=88/12)オクターブあります。そこで、鍵盤の中央に 440Hz のラを含むドレミファソラシドを配置したとすると、最低音域はこの4オクターブ程度下(7.3/2 = 3.6.. )にあることになります。
仮に、440Hz (ラ)の 4オクターブ下を求めると、\(440 Hz/2^4\) =27.50Hz となります。対応するドを求めると 16.35Hz となります。
人の可聴域は 20Hz から 20,000Hz といわれます。そこで、楽器で演奏できる下限、人が聴き取ることができる下限の領域をオクターブ番号0として定義すると、440Hz を含む音域のド、ラは、それぞれ、C4、A4 (440Hzのラ)となります。

MIDI ノートナンバーの導入

つぎに、f'(n) = f(n-69) として f'(n) を定義しなおし、(*07) を使うと、以下となります。

$$ f'(n) = 440・2^{(n-69)/12}, Hz … (*09)$$

つまり、n = 69 とすると、f’(69) = 440 Hz (基準音、A4)となります。
MIDI ノートナンバーの定義では、ノートナンバー n が 69 のとき、A4 に対応する( n = 60 のとき C4 に対応する)といった関係があります。
MIDI ノートナンバーを使う際は、この関係(n を 69 個分シフトする)を使います。

国際的表記、MIDI ノートナンバー、周波数の関係

上記の関係を一部数値で示すと、以下のようになります。

n -9 -8 -7 -6 -5 -4 -3 -2 -1 0 +1 +2 +3
MIDI n 60 61 62 63 64 65 66 67 68 69 70 71 72
音階 C4 C#4 D4 D#4 E4 F4 F#4 G4 G#4 A4 A#4 B4 C5
ド# レ# ファ ファ# ソ# ラ#
周波数 261.6 277.2 293.7 311.1 329.6 349.2 370.0 392.0 415.3 440.0 466.2 493.9 523.3

プログラムでは、前述の式からこの表の関係を求め、対応する周波数の音を生成しています。

440 Hz の基準音を確認してみる

上記のスクリプトが自由に扱えるようになると、いくつか実験が可能になります。
以下の文字列を [Scientific Pitch: ] 欄に貼りつけて waveform で “sine” 波を選択し、[play] ボタンをクリックしてみてください。

1.00 A4 
1.00 A4 
1.00 A4 
4.00 A5 

テレビなどでの時報の音です。A4 の 440Hz (基準音、ラの音)と 880Hz (A5、1オクターブ上)の純音を耳で確認できると思います。
上記のようなデータを作るのは簡単です。よく聞くメロディーやテスト用問題を作って音を覚えていき、絶対音感の練習をするなど、いくらでもアイディアを発展させることが可能だと思います。

三和音、四和音を導出してみる

ドミソなどの和音も、今回のソフトウェアを使って、実際に音を聞きながら導出していくことが可能です。
一例として、ピアノの中央のド C4 を例に取ります。C4 の周波数は 261.63 Hz です。

一般に、楽器や人の声、鳥、動物の鳴き声など、自然環境で聞く音には倍音、自然倍音が含まれます。
そこで、C4 の1倍音、2倍音、… 10倍音の周波数のリストを作ります。
周波数の数字を2倍、3倍、… にするだけです。

1.00 261.63 # 1倍音 C4 基本の音
1.00 523.26 # 2倍音 C5 1オクターブ上
1.00 784.89 # 3倍音 G5 
1.00 1046.52 # 4倍音 C6 2オクターブ上
1.00 1308.15 # 5倍音 E6 
1.00 1569.78 # 6倍音 G6 
1.00 1831.41 # 7倍音 約Bb6 
1.00 2093.04 # 8倍音 C7 3オクターブ上 
1.00 2354.67 # 9倍音 D7 
1.00 2616.30 # 10倍音 E7 

上記の音を再生する場合は、[Frequency notation:] 欄に上記を貼りつけて [play] ボタンをクリックしてみてください。また、純音で聞いたほうがよいと思いますので、waveform は sine に設定してください。

上記のリストでは、基本の音の1オクターブ上、2オクターブ上、… の音が含まれています。
和音を調べるにあたっては、同種の音は1つにまとめるのが妥当です。そこで、偶数倍音の行を削除します。6倍音の G6 も G5 があるので削除します。
また、4和音程度まで調べたいので(高次の倍音ほど一般に音量が小さくなっていくため)、9倍音以降も削除します。

1.00 261.63 # 1倍音 C4 基本の音
1.00 784.89 # 3倍音 G5 
1.00 1308.15 # 5倍音 E6 
1.00 1831.41 # 7倍音 約Bb6 

上記のリストを再生してみると、オクターブがかなり飛びすぎていることがわかります。歌えませんし、演奏も難しくなります。そこで、C4 と同じオクターブ番号となるように下げます。つまり、G5 であれば、周波数を2で割ります。E6 は 4で割ります。割り算をしたら、周波数の小さい順に並べ替えます。

1.00 261.63 # C4 基本の音
1.00 327.03 # E4 
1.00 392.45 # G4 
1.00 457.85 # 約Bb4 

前から3つを取ると、ドミソ(Cメジャーコード、C, E, G)の3和音になっています。
また、4つを取ると、ドミソシ♭(Cセブンスコード、C, E, G, B♭)の4和音になっています。
最後の 457.85 Hz は、ぴったり合う音階がなく、最も近い B♭4 (466.16 Hz) からは 2% 程度ずれています。(*08) より、周波数が 6%ずれると音階が1つずれてしまいますが、半音の半分以下なので、許容範囲といったところです。また、B♭4 のフラットを外すと、Cメジャーセブンスコード(C, E, G, B)が出てきます。

また、上では、1、3、5、7倍音という奇数倍音を抜き出して、オクターブ番号4に合わせましたが、オクターブ番号6に合わせると、周波数を4倍、2倍していけばよいことになります。
すると、3和音の周波数比は、4:5:6となります。4和音(≒セブンスコード)の周波数比は、4:5:6:7となることがわかります。
たとえば、A4 (440Hz) を基準に取ると、上記のコードの周波数の比は、440Hz : 550Hz : 660Hz : 770Hz となります。
また、3和音の4:5:6の最後の音を1オクターブ下げて並べなおすと、3:4:5になります。周波数でみると、3和音の周波数の比は、三平方の定理が成り立つ直角三角形を構成する関係になっています。
これらの音も上記のプログラムで確認することが可能です。

一般に、自然環境での音では低次の倍音ほど音の強度が強くなるので、Cを基準とするコードの場合、中心となるのは、1倍音と3倍音に対応した、CとGとなります(完全5度の関係となる C、G が中心軸となっている)。
他の E、B は倍音の強度としては付加的なものとなっています。この付加的な2音を半音さげたりせずにそのままとするか、あるいは、片方・両方を半音下げるかによって、場合分けが生じます。これにより、メジャーコードと、マイナーコードの派生が生まれてくることがわかります。

また、少し見方を変えると、上記の3、4和音を構成する音階(4:5:6:7となる音階)は、1オクターブを12段階に分けたとき、うまく表現できることがわかります。定義された音階のすべての範囲で自由に転調でき、転調後、3和音、4和音の基本和音がつねに含まれています。これにより、作曲や演奏の自由度や表現力が広がることとなり、17~18世紀以降、12平均律や西洋音楽が発展、普及していった理由が理解できます。

自然環境で聞く音には倍音が含まれており、この整数の組み合わせとなる音を聴くと調和して聴こえる。つまり、音の中に4:5:6などといった整数の組み合わせを見つけると脳が自然に反応する。
組み合わせが異なると、うなりなどが発生して不自然に感じる。こういったところも興味深いところです。

また、楽器や声で和音の確認をしようとすると、倍音、自然倍音が多数含まれてしまって、的確な判断や説明が難しくなります。しかし、今回のようにソフトウェアを使うと、和音の構成音を抽出してサイン波の純音で再生・確認できるなど、大きなメリットがあるといえます。
なお、プログラミングで和音、コードを鳴らすことも可能です。興味のある方は、下記の「Python で Just the Two of Us 進行 【コード進行】」なども参照してみてください。少しややこしいことをする場合は、Python などのプログラミング言語を使ったほうが簡単です。

スクリプトの説明

・ HTML ファイルの audioContext1 が、音声を生成するためのオブジェクトです。
・ bpm1、beat_length1 等で、BPM、一拍の長さ等を定義しています。
・ load1()、clear1() 等の関数で、起動時の処理、各ボタンをクリックしたときの処理を定義しています。
・ set1()、set2()、set3() が、画面の各欄で音階などを記載したときの処理を定義しています。
・ CDEFGAB_to_semitones1()、pitch2semitones1()、semitones2frequency1() で、国際表記の文字を半音の個数分(MIDI ノートナンバーに対応)、周波数への換算などを行っています。
・ tone1() 関数が音を鳴らす関数です。音を鳴らす時間と周波数を入れると、その期間だけ音を生成します。
・ play0()、play1() で、周波数のリストから、tone1() を実行することで、メロディーを生成します。
・ HTML の後半では画面の設定をしています。

※ なお、コンピュータミュージックでは一般い 440Hz で統一されています。
一方、オーケストラ演奏などでの楽器の場合、440Hz ではなく、442Hz に合わせることが一般的のようです。オーケストラ演奏などに合わせる場合は、プログラムで 440 となっているところを 442 に変更してください。

まとめ

ブラウザでメロディーを鳴らす JavaScript のプログラムについてまとめました。

また、プログラムを作るにあたり、音程やコードがどのように成り立っているのか、音楽と数学・物理の関係について一般教養の程度の範囲でまとめてみました。こうした背景がわかっていれば、ハードウェア環境やプログラミング言語が多少異なっていても、その場の状況に応じて自由に変換式を作って、欲しい機能を実現することが可能になります。
これで、パソコン1台程度があれば、特殊な機材がなくても簡単なメロディーなどを作成して演奏することが可能になりました。

なお、Python を使って、メジャーコード、マイナーコードなどの和音を演奏することも可能です。
また、上記と同等の要領で、ブラウザ上で電子ドラムを鳴らすことも可能です。何らの演奏技術がなくても、8ビートなどのドラムパターンを演奏することができます。8ビート、16ビートのドラムパターンを演奏することなんて今後の人生で一度もないであろうと思う方は、下記の関連リンクなども参照してみてください。

関連リンク
・ ブラウザで電子ドラムを鳴らす 【JavaScript】
・ ドレミファソラシドを鳴らすサンプル【Python】
・ キーボードピアノ 【本格49鍵!】
・ Python で Just the Two of Us 進行 【コード進行】

サンプルスクリプト

melody_player1.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>melody player v0.1</title>
</head>
<body>
    <script src="./play_pattern1.js"></script>
    <script>

    let audioContext1 = new (window.AudioContext || window.webkitAudioContext)();
    let bpm1 = 60;                                                     // beats per minute 
    let beat_length1 = 60.0/bpm1 * 1000.0;                             // beat length, ms 
    let vol1 = 1.0;                                                    // volume 0.0 - 1.0 
    let type1 = "sawtooth"; 
    let str1_js = "1.00 C4 \n1.00 D4 \n1.00 E4 \n1.00 F4 \n1.00 G4 \n1.00 A4 \n1.00 B4 \n1.00 C5 \n"; // initial sample 
    let pattern1 = [];                                                 // note pattern 
    let damping1 = true; 
    let stop_flag1 = -1; 

    window.onload = load1; 
    function load1() { 
        if (typeof str_pattern !== 'undefined') { str1_js = str1_pattern.trim(); } 
        document.getElementById("textarea1").value = str1_js; 
        document.getElementById("select1").selectedIndex = 0; 
        document.getElementById("select2").selectedIndex = 2; 
        select1(); 
        set1(); 
    } 

    function clear1() { 
        document.getElementById("textarea1").value = ""; 
        document.getElementById("textarea2").value = ""; 
        document.getElementById("textarea3").value = ""; 
    } 

    function download1() { 
        let file1 = "play_pattern1.js" 
        let str1 = document.getElementById("textarea1").value.trim();
        str1 = "window.str1_pattern = `\n" + str1 + "\n`;\n" 
        let blob1 = new Blob([str1], {type: 'text/plain'});
        let link1 = document.createElement('a');
        link1.href = URL.createObjectURL( blob1 );
        link1.download = file1; 
        link1.click(); 
    } 

    function select1() { 
        let obj1 = document.getElementById("select1"); 
        let vol2 = parseFloat(obj1.options[obj1.selectedIndex].value); 
        document.getElementById("textbox1").value = vol2; 
        textbox1(); 
    } 

    function select2() { 
        let obj1 = document.getElementById("select2"); 
        type1 = obj1.options[obj1.selectedIndex].value; 
        document.getElementById("textbox3").value = type1; 
    } 

    function textbox1() { 
        let v1 = parseFloat(document.getElementById("textbox1").value); 
        if (v1 < 0.0) { 
            v1 = 0.0; 
            document.getElementById("textbox1").value = "0"; 
        } else if (v1 > 100.0) { 
            v1 = 100.0; 
            document.getElementById("textbox1").value = "100"; 
        } 
        vol1 = v1/100.0; 
    } 

    function textbox2() { 
        bpm1 = parseInt(document.getElementById("textbox2").value.trim()); 
        beat_length1 = 60.0/bpm1 * 1000.0;                             // ms  
    } 

    function checkbox1() { 
        damping1 = document.getElementById("checkbox1").checked;       // damping, true or false 
    } 

    function textarea1() { set1(); } 

    function textarea2() { set2(); } 

    function textarea3() { set3(); } 

    function set1() { 
        select2(); 
        checkbox1();                                                   // damping  
        textbox2();                                                    // bpm and beat length 
        let a1 = document.getElementById("textarea1").value.trim().split("\n\n\n")[0].replace(/\n\n/g, "\n").split("\n"); 
        for (let i1 = 0; i1 < a1.length; i1++) { 
            a1[i1] = a1[i1].trim().split(" "); 
        } 

        let str1 = ""; 
        for (let i1 = 0; i1 < a1.length; i1++) { 
            let str3 = a1[i1][0]; 
            let str4 = ""; 
            if (isNumeric1(str3)) { 
                v1 = parseFloat(str3); 
                a1[i1][0] = v1; 
                str3 = v1.toFixed(2); 
            } 
            if (a1[i1].length > 1) { 
                str4 = a1[i1][1]; 
                if (isNumeric1(str3)) { 
                    if (isNumeric1(str4)) { 
                    } else { 
                        v2 = pitch2semitones1(str4);                       // MIDI notes number 
                        str4 = v2; 
                    } 
                } 
            } 
            str1 = str1 + str3 + " " + str4 + " \n"; 
        } 
        document.getElementById("textarea2").value = str1; 
        set2(); 
    } 

    function set2() { 
        let a1 = document.getElementById("textarea2").value.trim().split("\n"); 
        for (let i1 = 0; i1 < a1.length; i1++) { 
            a1[i1] = a1[i1].trim().split(" "); 
        } 
        str1 = ""; 
        for (let i1 = 0; i1 < a1.length; i1++) { 
            
            if (a1[i1].length == 1) { 
                str1 = str1 + a1[i1][0] + " \n" 
            } else if (isNumeric1(a1[i1][0])) { 
                v1 = a1[i1][1]; 
                v2 = semitones2frequency1(v1); 
                str1 = str1 + a1[i1][0] + " " +v2.toFixed(3) + " \n" 
            } else { 
                str1 = str1 + a1[i1][0] + " " + a1[i1][1] + " \n" 
            } 
        } 
        document.getElementById("textarea3").value = str1; 
        set3(); 
    } 

    function set3() { 
        let a1 = document.getElementById("textarea3").value.trim().split("\n"); 
        for (let i1 = 0; i1 < a1.length; i1++) { 
            a1[i1] = a1[i1].trim().split(" "); 
        } 
        pattern1 = a1; 
    } 

    function CDEFGAB_to_semitones1(str1) { 
        let map1 = { C:-9, D:-7, E:-5, F:-4, G:-2, A: 0, B: 2 } 
        return map1[str1.toUpperCase()]; 
    } 

    function pitch2semitones1(str1) { 
        let c1 = str1.slice(0, 1);                                     // first character 
        let c2 = str1.slice(1, 2);                                     // second character 
        let c3 = str1.slice(-1);                                       // last character 
        let n1 = CDEFGAB_to_semitones1(c1);                            // relative semitones 
        let n2 = (parseInt(c3)-4)*12; 
        let n3 = n1 + n2 + 69; 
        if (c2 == "s") { 
            n3 = n3 + 1; 
         } else if (c2 == "#") { 
            n3 = n3 + 1; 
         } else if (c2 == "♯") { 
            n3 = n3 + 1; 
         } else if (c2 == "f") { 
            n3 = n3 - 1; 
         } else if (c2 == "♭") { 
            n3 = n3 - 1; 
         } else if (c2 == "b") { 
            n3 = n3 - 1; 
        } 
        return n3; 
    } 

    function semitones2frequency1(n1) { 
        let v1 = 440.0 * 2 ** ((parseInt(n1)-69)/12);                              // A4 = 440Hz 
        return v1; 
    } 

    function isNumeric1(str1) { 
        return Number.isFinite(Number(str1)); 
    } 

    function tone1(duration1, frequency1) { 
        let duration2 = parseFloat(duration1); 
        let oscillator1 = audioContext1.createOscillator();
        let gain1 = audioContext1.createGain(); 
        oscillator1.connect(gain1);
        gain1.connect(audioContext1.destination); 
        oscillator1.type = type1; 
        oscillator1.frequency.value = parseFloat(frequency1); 
        gain1.gain.setValueAtTime(vol1, audioContext1.currentTime);                                  // volume 
        oscillator1.start();
        if (damping1 == true) { 
            gain1.gain.exponentialRampToValueAtTime(0.001, audioContext1.currentTime + duration2);   // damping 
        } 
        oscillator1.stop(audioContext1.currentTime + duration2);
    } 

    function wait1(t1) { 
        return new Promise(resolve => setTimeout(resolve, t1));
    }

    async function play0() { 
        await audioContext1.resume();                                                                // initialize 
        let n1 = -1; 
        for (let i1 = 0; i1 < pattern1.length; i1++) { 
            if (stop_flag1 > 1) { 
                break; 
            }  
            n1 = n1 + 1; 
            let v1 = pattern1[n1][0]; 
            if (v1 == "set_BPM1") { 
                let v2 = parseInt(pattern1[n1][1]); 
                if (v2 > 0) { 
                    bpm1 = v2; 
                    beat_length1 = 60.0/bpm1 * 1000.0; 
                } 
            } else if (v1 == "set_volume1") { 
                let v2 = parseInt(pattern1[n1][1]); 
                if (v2 > -1 && v2 < 101) { 
                    vol1 = v2/100.0; 
                } 
            } else if (v1 == "set_waveform1") { 
                let str1 = pattern1[n1][1].trim(); 
                if (str1 == ("sine" || "square" || "sawtooth" || "triangle")) { 
                    type1 = str1; 
                } 
            } else if (isNumeric1(v1)) { 
                if (pattern1[n1].length > 1) { 
                    let v2 = pattern1[n1][1]; 
                    tone1(v1, v2); 
                } 
                await wait1(v1*beat_length1); 
            } 
        } 
        stop_flag1 = -1; 
    } 

    function play1() { 
        if (stop_flag1 < 0) { 
            textbox1(); 
            textbox2(); 
            select2(); 
            stop_flag1 = 1; 
            play0(); 
        } 
    } 

    function stop1() { 
        if (stop_flag1 == 1) { 
            stop_flag1 = 2; 
        } 
    } 

    </script> 

    <h1>melody player</h1>
    <button onclick="play1()">play</button> 
    <button onclick="stop1()">stop</button> 
    <button onclick="set1()">set</button> 
    <button onclick="clear1()">clear</button> 
    <button onclick="download1()">download</button>  
    <input type="checkbox" id="checkbox1" name="checkbox1" onchange="checkbox1()" checked><label for="checkbox1">damping</label><br> 

    <input type="text" id="textbox1" style="width:60px; height:14px; margin-right:-5px;" onchange="textbox1()">
    <select id="select1" style="width:18px;" onchange="select1()">
        <option value="100">100</option>
        <option value="90">90</option>
        <option value="80">80</option>
        <option value="70">70</option>
        <option value="60">60</option>
        <option value="50">50</option>
        <option value="40">40</option>
        <option value="30">30</option>
        <option value="20">20</option>
        <option value="10">10</option>
        <option value="0">0</option>
    </select> : volume     
    <input type="text" id="textbox2" style="width:60px; height:14px;" value="60" onchange="textbox2()"> : BPM    
    <input type="text" id="textbox3" style="width:60px; height:14px; margin-right:-5px;" readonly>
    <select id="select2" style="width:18px;" onchange="select2()">
        <option value="sine">sine</option>
        <option value="square">square</option>
        <option value="sawtooth">sawtooth</option>
        <option value="triangle">triangle</option>
    </select> : waveform   

    <br><br>
    <div style="display:flex;">
    <div>
    Scientific Pitch: <br>
    <textarea id="textarea1" name="textarea1" value="" rows="20" cols="20" style="background-color:#ffffff " onchange="textarea1()" ></textarea>
    </div>
    <div>
    MIDI notation: <br>
    <textarea id="textarea2" name="textarea2" value="" rows="20" cols="20" style="background-color:#ffffff " onchange="textarea2()" ></textarea>
    </div>
    <div> 
    Frequency notation: <br>
    <textarea id="textarea3" name="textarea3" value="" rows="20" cols="20" style="background-color:#ffffff " onchange="textarea3()" ></textarea><br>
    </div>
    </div>
    <div id="div1"></div>

</body>
</html>

play_pattern1.js

window.str1_pattern = `
1.00 C4 
1.00 D4 
1.00 E4 
1.00 F4 
1.00 G4 
1.00 A4 
1.00 B4 
1.00 C5
`;
タイトルとURLをコピーしました