パンデミック後の新世界を作るために (foussin’s blog)

(『見捨てられた世代』からの提言)

MSX-BASIC で音のドップラー効果を再現(長文)

 今回は、前回のエンジン音に、距離に比例して音が小さくなる仕組みを追加。さらにドップラー効果も加えている。エンジン始動時のセルモーターを回す時の「キュルキュル音」の再現にも挑戦している(イマイチだけど)。

 是非とも MSX実機で実行してほしい。110行の fclock を変更すれば、MSX以外の機種でも実行できるかもしれない。まあ、PSG(SSG) の性能に依存するとは思うが。

10 'written by foussin
20 'エンジン & キョリ2ジョウ & ドップラ-コウカ
30 '(MSX2+ ヒョウジュン・モ-ド)
40 '2019.3.9sat - 2019.3.15fri
50 :
70 '     12345678
80 'save"doppler .bas",a
90 :
100 'init
110 FC=1789772.5# 'fclock(MSX ノ バアイ)
120 SOUND8,0:SOUND9,0:SOUND10,0
130 :
140 CLS:PRINT"イグニッション!"
150 SOUND 7,&B110110 'noiseA,toneA
160 SOUND 8,16:A=1   'A(A=1),B(A=0)
170 SOUND13,14       '10,14(ワウワウsound)
180 I=1600           'I(rpm)
190 GOSUB 1380       'noise,tone sub:
200 I6=20:R6=I6
210 FOR J=0 TO 2
220  SOUND 6,R6
230  R6=(R6=0)*-I6
240  FOR T=0 TO 60:NEXTT
250 NEXT J
260 SOUND 8,0:FOR T=0 TO 20:NEXTT
270 :
280 'エンジン・キドウ->アイドリング  カンセイ ニチジ
290 '1200->3600->600(rpm)  (2019.3.2)
300 SOUND 7,&B110101'noiseA,toneB(A=0)
310 SOUND 6,10:SOUND 8,16:SOUND 9,11
320 SOUND13,8:A=0
330 I=1200:GOSUB 1380 'noise,tone sub:
340 GOSUB 1320        'print sub:
350 FOR T=0 TO 70:NEXTT
360 FOR I=3600 TO 1200 STEP-300
370  GOSUB 1380       'noise,tone sub:
380  IF I MOD 300=0 THEN GOSUB 1320
390 NEXT I
400 :
410 PRINT:PRINT
420 PRINT"アイドリング!(タンキトウ・エンジン ノ オト)"
430 I=600:GOSUB 1380  'noise,tone sub:
440 GOSUB 1320        'print sub:
450 FOR T=0 TO 800:NEXTT
460 :
470 PRINT:PRINT:PRINT"カウントダウン!:OOOO";
480 X0=POS(0):Y0=CSRLIN
490 SOUND 7,&B110000 'noiseA,toneABC
500 SOUND 8,16:SOUND 9,12:SOUND 10,0
510 I=600:GOSUB 1380 'noise,tone sub:
520 J=1:K=3:HZ=440
530 ON INTERVAL=60 GOSUB 560
540 INTERVAL ON:GOTO 660 'skip
550 :
560 'interval timer sub:**************
570 LOCATE X0,Y0:PRINT K;
580 X=X0-J:J=J+1:CT=0
590 LOCATE X,Y0:PRINT"*";
600 IF K=0 THEN WT=300:HZ=880
610 TP=FC/(16*HZ):K=K-1
620 C5=INT(TP/256):F4=INT(TP-(C5*256))
630 SOUND 5,C5:SOUND 4,F4 'ch-C
640 SOUND10,12:RETURN
650 :
660 IF K<0 THEN 700
670 IF CT=1 THEN 660
680 FOR T=0 TO 60:NEXTT:SOUND10,0
690 CT=1:GOTO 660
700 INTERVAL OFF
710 :
720 PRINT:PRINT:Y=CSRLIN 'スタ-ト!
730 'ギア1-4:カソクド(イドウキョリ) ト タイオウ サセル
740 '      :ボリュ-ム ト キョリ2ジョウ ヲ タイオウ
750 '      low   high       サイコウソクド
760 ' 1st: 2100- 7200(rpm)  120km/h(Hz)
770 ' 2nd: 4200-10500       175km/h
780 ' 3rd: 7500-13200       220km/h
790 ' 4th:11400-16500       275km/h
800 ' 5th:14400-19200       320km/h
810 ' [topギア(5th) ハ ロング・ストレ-ト センヨウ]
820 SOUND 6,12:DIM LO(5),HI(5)
830 LO(1)=2100 :LO(2)=4200 :LO(3)=7500
840 LO(4)=11400:LO(5)=14400
850 HI(1)=7200 :HI(2)=10500:HI(3)=13200
860 HI(4)=16500:HI(5)=19200
870 :
880 'キョリ2ジョウニ ヒレイシテ オトヲ チイサクスル ジュンビ
890 TP=.6 'TP(km:オリカエシテン:Turnin'Point)
900 CT=1      'CT(オリカエシ カウンタ)
910 A=13:B=10 'volume:A(4-13) B(1-10)
920 S=0 :RS=0 'S(ソウコウキョリ) RS(ソウタイキョリ)
930 TM=0:TB=1 'TM(time) TB(Turn Back)
940 CM$=" (チカヅク) " 'coming
950 GO$=" (トオザカル)" 'going
960 :
970 ON INTERVAL=28 GOSUB1650'print sub:
980 INTERVAL ON
990 SOUND8,A:SOUND9,B
1000 FOR J=1 TO 5     'オ-トマ・ギアシフトアップ
1010  FOR I=LO(J) TO HI(J) STEP 330
1020   GOSUB 1500     'tone sub:
1030  NEXTI
1040  IF J=CT THEN 1040
1050 NEXTJ
1060 J=5:I=19200      'サイコウソク デ シュウカイ
1070 GOSUB 1500       'tone sub:
1080 IF CT=7 THEN 1100 ELSE 1070
1090 :
1100 INTERVAL OFF:SOUND 7,&B111000'=56
1110 SOUND 8,0:SOUND 9,0:SOUND10,0
1120 AG=INT(S/TM*3600+.5):PRINT
1130 PRINTUSING"ヘイキン ソクド:###km/h";AG
1140 END
1150 :
1160 '** ドップラ-コウカ(doppler effect) **
1170 'f0:シュウハスウ(Hz)
1180 'v :オンゲンガ ウゴク ハヤサ(m/s or km/h)
1190 'V :オンソク     (340m/s or 1224km/h)
1200 '            (タダシ V>v トスル)
1210 'f :キコエル オトノ シュウハスウ(モトメル アタイ)
1220 '
1230 'オンゲン ガ チカヅクトキ(キクヒト ハ セイシ)
1240 ' f=V/(V-v)*f0
1250 '
1260 'オンゲン ガ トオノク トキ(キクヒト ハ セイシ)
1270 ' f=V/(V+v)*f0
1280 '
1290 ' f=1224/(1224+FE*TB)*FE
1300 ' コレデOK!:FE=シュウハスウ(Hz)=ジソク(km/h)
1310 :
1320 'print sub1(ニュ-トラル):**************
1330 Y=CSRLIN:LOCATE 0,Y
1340 PRINTUSING"N:#####rpm";I;
1350 PRINTUSING"(##Hz)";INT(FE+.5);
1360 RETURN
1370 :
1380 'noise & tone sub:(エンベロ-プ)******
1390 FE=I/60:EP=FC/(256*FE)
1400         CT=INT(EP/256)
1410         FT=INT(EP-(CT*256))
1420   TP=FC/(16*FE)
1430   C4=INT(TP/256)
1440   F8=INT(TP-(C4*256))
1450 SOUND12,CT:SOUND11,FT 'ch-A
1460 IF A=1 THEN SOUND1,C4:SOUND0,F8 'A
1470 IF A=0 THEN SOUND3,C4:SOUND2,F8 'B
1480 RETURN
1490 :
1500 'tone sub(doppler effect):********
1510 INTERVAL STOP
1520 FE=I/60:DP=1224/(1224+FE*TB)*FE
1530 FP=FC/(16*DP)           'ch-A
1540 C1=INT(FP/256):F0=INT(FP-(C1*256))
1550 FP=FC/(16*(DP-20))      'ch-B
1560 C3=INT(FP/256):F2=INT(FP-(C3*256))
1570 SOUND 1,C1:SOUND 0,F0   'ch-A
1580 SOUND 3,C3:SOUND 2,F2   'ch-B
1590 IF I>3500 THEN SOUND10,0'ch-C(off)
1600 INTERVAL ON:RETURN
1610 :
1620 'print,volume sub:(インタ-バル・タイマ)***
1630 '0.5sec ゴトニ S,RS(キョリ) ヲ ケイソク
1640 'maxVolume/(rs*10)^2+minVolume
1650 TM=TM+.5:Q=FE/7200:S=S+Q
1660 RS=RS+Q*TB
1670 IF RS>TP OR RS<0 THEN GOSUB 1840
1680 IF TB=1 THEN RE$=GO$ ELSE RE$=CM$
1690 SS=RS*RS*100+2 '+2(ビチョウセイ)
1700 A=INT(13/SS+9.5)
1710 B=INT(10/SS+6.5)
1720 IF A>13 THEN A=13:B=10
1730 SOUND8,A:SOUND9,B
1740 LOCATE 0,Y
1750 PRINTUSING"#:#####rpm";J;I;
1760 PRINTUSING"(###km/h)";INT(FE+.5);
1770 PRINTUSING":###.#sec";TM
1780 PRINTUSING"  ソウコウキョリ:#.##km";S
1790 PRINTUSING"  ソウタイキョリ:#.##km";RS;
1800 PRINT RE$:RETURN
1810 :
1820 '(RS>TP) or (RS<0) sub:**********
1830 '(ドップラ-コウカ ノ カワリメ ノ ショリ)
1840 IF RS>TP THEN 1860 ELSE 1850
1850 RS=ABS(RS):TB=1:ST=-1:GOTO 1890
1860 RS=TP-(RS-TP):TB=-1:ST=1:CT=CT+1
1870 IF CT=7 THEN RETURN
1880 :
1890 Q0=1224/(1224+FE)*FE 'トオノク(low)
1900 Q1=1224/(1224-FE)*FE 'チカヅク(high)
1910 IF TB=1 THEN SWAP Q0,Q1
1920 Z0=0
1930 Z0=Z0+1:Z=FE+(Z0^2+J)*ST
1940  FP=FC/(16*Z)               'ch-A
1950  C1=INT(FP/256):F0=INT(FP-C1*256)
1960  FP=FC/(16*(Z-20))          'ch-B
1970  C3=INT(FP/256):F2=INT(FP-C3*256)
1980  IF ST=1 AND Z>Q1 THEN 2020
1990  IF ST=-1 AND Z<Q1 THEN 2020
2000 SOUND1,C1:SOUND0,F0         'ch-A
2010 SOUND3,C3:SOUND2,F2:GOTO1930'ch-B
2020 RETURN

 今回は、上記のコードが書かれたプロセスを詳しく紹介する。コードを書いたのは2年前だが、今の自分は Windows10 マシンが故障中なので、MSX-BASIC, MSX-C でコーディングしている。興味がある人は各自研究してみて。

 まずは「ドップラー効果」について。

ドップラ-効果:doppler effect
--------------------------------------
  f0 振動数
     (単位時間が秒なら周波数を意味する)
  u  音源が動く速さ
  v  聞く人が動く速さ
  V  音速(空気中の音波の速さ)
     (ただし V>u, V>v とする)
  l  音波の波長(lambda:ラムダ[λ])
--------------------------------------

聞こえる音波の振動数 f を求める:

聞く人が静止して音源が動く場合:
・音源が近づく時:
  f = V/l = V/(V-u) * f0
・音源が通り過ぎる瞬間:
  f = f0
・音源が遠ざかる時:
  f = V/l = V/(V+u) * f0

 音速を 340m/s(1224km/h)として、F1マシンがエンジン回転数19200rpm、時速315km で通り過ぎる場合を考える。rpm は毎分回転数なので、これを毎秒回転数 rps = rpm/60 に直す。1秒間あたりの振動数を周波数(Hz)と呼ぶので rps = Hz と解釈できる。これが f0 となる。

  f0 = 19200/60 = 320 rps = 320 Hz
 これを上記の式に当てはめれば...

 ・近づく時:f = V/(V-u) * f0
    1224/(1224-315) * 320 = 約431 Hz
 ・遠のく時:f = V/(V+u) * f0
    1224/(1224+315) * 320 = 約255 Hz

...となる。F1 マシンの場合、近づく時と遠のく時の差が約 180 Hz もあり、近所で体験できる普通車のドップラ-効果とは雲泥の差がある。時速60キロで走っている一般車のドップラ-効果の差は、せいぜい 10Hz ぐらいしかない。

音源が静止して聞く人が動く場合:
・聞く人が近づく時:
  f = (V+v)/l = (V+v)/V * f0
・聞く人が通り過ぎる瞬間:
  f = f0
・聞く人が遠ざかる時:
  f = (V-v)/l = (V-v)/V * f0

 仮に、聞く人が音速を超えて動いていた場合は、音源の音波をどこかで追い越してしまうので、途中から音が一切聞こえなくなる。音波を追い越す瞬間は、音源の波長がものすごく引き伸ばされるので、予想外の重低音が聞こえるはず。
 要するに、非常に極端なドップラ-効果を体験するが、超音波になって人の耳には聞こえないと思う。

 仮に、音源が音速を超えて動いていた場合は、音源が聞く人を追い越し、しばらく経つまで音は一切聞こえない。で、音が聞こえると同時に衝撃波(ソニックブ-ム)も届く。


エンジン音の再現には「波」の知識が必要:

 車のエンジン音は rpm (毎分回転数)を使えばシミュレ-トできる。これはピストンの振動数と言える。これを毎秒回転数で表現すると、それは「エンジンの周波数」と解釈することができる。

毎分回転数:rpm(revolution per minute)

毎秒回転数(rps) = rpm/60 = 周波数(Hz)

 ところで、エンジン音は特定の周波数を持たないノイズでありながら、アクセルを踏み込んで回転数を上げると音が高くなる。これは人間の耳が、エンジン音(ピストンの振動数)を音の周波数として知覚するためだ。

 波の波長と振動数は必ず反比例する。仮に比例関係になるためには、波の伝わる速さが変わる必要がある。が、波の速さは媒質の性質で決まってしまうので、それはあり得ない。で、エンジン回転数(振動数)を上げると波長は逆に短くなり、音が高くなる。波長と振動数が反比例するのは、あらゆる波に当てはまる(電磁波も然り)。

 MSXサウンド・ジェネレ-タ(PSG音源)では「ジェットエンジン」の音も再現できる。例の「キ--ン」という音がジェットエンジンの音だ。
 あの音はエンジンの毎分回転数が 8万~30万rpm ぐらいで聞こえ始める。間を取って 20万rpm を毎秒回転数に直すと 約3300rps となる。rps = Hz なので 3300Hz の音が聞こえる計算になる。で、ジェットエンジンの音は 1300~5000Hz 程度の周波数ということになる。

 3300Hz をMML(ミュージック・マクロ・ランゲージ)の音階に直すと "o7A" ぐらいに相当する。大したことないと思うかもしれないが、甲高い音と思われている F1 サウンドでさえ、せいぜい 320Hz("o4E" 程度)ぐらいだ(時速300km のドップラ-効果を加えると 430Hz ぐらいで聞こえる)。
 エンジン音にはノイズも大量に混ざっているし、何よりも音量が桁違いに大きいので(爆発音だし)、楽器や肉声の聞こえ方とは雰囲気が異なるのだ。


距離の 2乗に比例して音を小さくする:

 ドップラ-効果をリアルに再現するには「距離の 2乗に比例して音が小さくなる」効果も必要になる。音源と聞く人の距離によって音は小さく聞こえたり大きく聞こえたりする。これは次の式で表現できる。

  maxVolume / s^2 + minVolume

  s:音源と観測者の距離(m or km)
  maxVolume:最大音量
  minVolume:最小音量

 距離 s の単位は km でも m でも構わない。maxVolume, minVolume は PC のボリュ-ムの範囲内を指定する。MSX のボリュ-ムの範囲は 0~15(4bit) なので、例えば次のように記述すれば良いはず...

  int(13/s^2+6.5)  '.5 は四捨五入用

 だが、s<1 (1未満) だと計算結果が最大音量(13)を超えてしまうので if 文で場合分けする必要がある。さらに s=0 だと division by 0 エラ-が発生するので、トラップを仕込んでおく必要もある。

 まあ、観測者と音源の距離が 0 になるということは、観測者と音源が衝突することを意味するので、通常はある程度の距離まで近づいたら、0 になる前に遠ざかり始めるようにコ-ド化すれば、トラップを用意する必要はない。


音量の単位は dB:

 ところが、場合分けやトラップを仕込んでおいても、この指定ではうまくいかない。その理由は「音量の単位」にある。音量の単位には dB(デシベル) が使われるが、この dB は「対数(log)スケ-ル」になっている。正確には、dB は音量の単位ではないが詳細は省く。要するにボリュ-ム範囲が 0~15 というのは、実は...

  2^0 ~ 2^15 (1 ~ 32768)

...を意味する。で、そうやって計算した結果を log() 関数に渡せば 0~15 の範囲内に戻すことができる。つまり次のように記述する(場合分け、トラップは省略)。

  'maxV:2^13, minV:2^6, s:1以上(m)
  a0 = 2^13 / s^2 + 2^6
  a = int(log(a0))
  sound 8,a

 このコ-ドを、s(距離) を変化させるル-プ中に置けば、かなりリアルな音量変化が見込める。が、このコ-ドにも問題がある。
 それは MSX の CPUクロック周波数が約 2~4 MHz しかないので、ル-プ中に複雑な数学関数を使用すると露骨に実行速度が遅くなってしまうのだ。これでは残念ながらリアルなシミュレ-ションは望めない。MSX turboR(16bit CPU) ならスム-ズに実行できると思うが...

 MSX-BASIC では、if, print, log, exp, ^(べき乗演算子) などをル-プ中で多用すると、露骨に実行速度が遅くなってしまう。最低限の実行速度は確保したいので、正確さの追求は諦め、適当にごまかす方法を 2通り考えた。

  '1: s(0~0.8 km) dt(微調整 定数)
  'int(maxV - s^2 * dt +.5)
  a=int(13-s*s*3+.5)       '文例

  '2: s(0.1~0.8 km)
  'int(maxV / (s*10)^2 + minV +.5)
  a=int(13/(s*s*100)+6.5)  '文例

 1: の方法では division by 0 エラ-を気にしなくて済むが、距離が大きくなると結果が負数になる可能性がある。微調整定数はトラップの条件設定次第で変える。問題は 2次関数的な音量変化があまり感じられないところ。そこがちょっと不満。

 2: では、結果が maxV を超えない工夫として 10^2 を掛け合わせた。これによって 100m(0.1km)単位で 1, 1/4, 1/9... と音量変化が適用される。距離が 0.1km 未満だと maxV を超えるので何らかのトラップが必要になる。0 除算トラップも必要だが、わりと自然な音量変化が実現できる。非力な 8bit CPU の PC なら、これはお薦めの式だと思う。

 距離の上限は 0.8 km ぐらいが適当と思われる。それ以上は音量が小さくなりすぎて、よく聞こえなくなるからだ。距離が遠くなりすぎる場合は minVolume がセットされ、それ以上音が小さくなることはない。距離の上限は、エラ-になるとかそういう理由で設けているのではない。


MSX-BASIC は遅いので、MSX-C を試してみた:

 log() 関数を使ってリアルな音量変化を再現しようとすると、実行速度が極端に遅くなって使い物にならなかった。そこで MSX-C を使ってみようと思って、ちょっと調べてみた。結論から言うと、MSX-C は使えない... 役不足だった。

 書籍「MSX-C 入門」上・下巻には sound文と同等の sound() 関数が「特製ライブラリ」にて用意されていた。が、もっと根本的な仕様に問題があった。それは、扱える数値が int or unsigned 限定というところ。つまり 2バイト整数(16bit)しか扱えない。

-32768~32767 (符号付き整数:int型)
     0~65535 (符号無し整数:unsigned型)

 実数が使えないので、log(), sin() などの数学関数が使えない。当然、実装もされていない。log() が使えないので「音量が距離 2乗に反比例する」シミュレ-ションも実現できそうにない。試しに、整数で「割り算」「剰余」を計算したら次のようになった。

ソ-スコ-ド(test.c):

#include <stdio.h>

/* test.c (2019.3.28thu) */

main() {
  int i;  /* ヘンスウセンゲン ハ セントウ デ ヤル */
  char p; /* % キゴウ (percent) */

  puts("セイスウ(int)デ ワリザン(/) ");
  puts("& ジョウヨ(%)\n");

  for (i = 1; i < 10; ++i) {
    printf("9 / %d = %d : ", i, 9 / i);
    printf("9 %% %d = %d\n", i, 9 % i);
  }
  putchar('\n');
  p = '%';
  for (i=1; i<10; ++i) {
    printf("7/%d = %d : ",i,7/i);
    printf("7%c%d = %d\n",p,i,7%i);
  }
}

実行結果(リダイレクト出力):

セイスウ(int)デ ワリザン(/) & ジョウヨ(%)
9 / 1 = 9 : 9 % 1 = 0
9 / 2 = 4 : 9 % 2 = 1
9 / 3 = 3 : 9 % 3 = 0
9 / 4 = 2 : 9 % 4 = 1
9 / 5 = 1 : 9 % 5 = 4
9 / 6 = 1 : 9 % 6 = 3
9 / 7 = 1 : 9 % 7 = 2
9 / 8 = 1 : 9 % 8 = 1
9 / 9 = 1 : 9 % 9 = 0

7/1 = 7 : 7%1 = 0
7/2 = 3 : 7%2 = 1
7/3 = 2 : 7%3 = 1
7/4 = 1 : 7%4 = 3
7/5 = 1 : 7%5 = 2
7/6 = 1 : 7%6 = 1
7/7 = 1 : 7%7 = 0
7/8 = 0 : 7%8 = 7
7/9 = 0 : 7%9 = 7

 実際に試してみたら、この程度のコンパイルでも 1分ぐらい待たされた。全ての工程を ramdisk で処理すればもっと高速化できるが、lib\*.rel, include\*.h ファイルまで全部 ramdisk に格納するのはちょっと無理。。。

 ドップラ-効果を再現するなら、周波数を 12bit(音声周波数), 16bit(エンベロ-プ周期)に分周し、2つのレジスタに格納する必要がある。が、fclock が 1789772.5 Hzあるので、int 型ではちょっと無理。さらに、実数や log() が使えないので dB <=> vol 変換をするのも絶望的だ。

 符号無し整数を 2つ使って「上位桁」「下位桁」に分けて計算する方法を考えれば、整数型だけでも処理できると思うが、自分にはちょっと荷が重い。あと、計算ル-チンが複雑になると高速実行が売りの C 言語の恩恵が感じられないかもしれない。。。

 書籍によると、別売の「MSX-C ライブラリ」を使えば、実数や倍精度整数が使えるらしい。「MSXマガジン 永久保存版1~3」の付属ディスクとかに入ってないかと探してみたが、無いみたい。。。

 MSX-BASIC の数値演算ル-チンは「Math-Pack」と呼ばれる ROM-BIOS に格納されていて、これらを呼び出すことで実数演算や数学関数を利用できる。cos,sin,log,sqr,rnd 等の関数もここに格納されている。

 実数表現には「単精度」と「倍精度」があり、単精度(6桁)は 4バイト、倍精度(14桁)は 8バイトで表現される。
 オペランドを格納する DAC(f7f6H),ARG(f847H) は共に 16bit(2バイト)サイズだが、BCD型式の浮動小数点で格納されるので実数が使える。


 なんか、途中から MSX-C の話題に脱線してしまったが、非力な MSX でリアルな音声出力を実現するには、色々と単純化(というかズル)が必要になる。
 例えば「周波数(Hz)=時速(km/h)」として画面表示しているのも、その1つだ。本来ならトルク計算をして馬力と速度を関連付けすべきだが、MSX では荷が重いのでズルをした訳。


 ところで最近、フロッピードライブを酷使し過ぎたようで「リード/ライト・エラー」が頻発し始めた。MSX のフロッピードライブは「ベルトドライブ駆動」なので、長期間使っているとゴムベルトが伸びきってしまうのだ。
 MSXフロッピーディスクは、今となっては「古文書」みたいなもの。貴重なデータなので壊したくない。ベルトを手配し、交換するまでは MSX をしばらく休眠させよう。

 そうなると、ネカフェでテキスト入力する必要があるが、それはそれでキツイ。本気で「中古ノーパソ」を探して見るか。。。
 こういう記事を「はてなブログ」で書くなら、やっぱり「見たまま入力」よりも「はてな記法」の方が断然便利だ。今回はこんなところ。以上。