Propeller日記 (4)

長嶋 洋一


Propeller日記(1)

Propeller日記(2)

Propeller日記(3)

Propeller日記(5)

続・Propeller日記(1)

続・Propeller日記(2)

続・Propeller日記(3)

続・Propeller日記(4)

続・Propeller日記(5)

続々・Propeller日記(1)

続々・Propeller日記(2)

続々・Propeller日記(3)

続々・Propeller日記(4)

2008年3月26日(水)

さて、昨日の続きである。 VocalTractオブジェクトの中のstartメソッドの、 以下の初期化の部分はまさに、サウンド出力の「肝」だったのである。 これをたまたま見落としていたのは偶然とはいえ、凡ミスであった。 まぁ、そのお陰でPropellerのアセンブラにだいぶ慣れてきたので、 損な回り道ということでもないだろう。
	ctrb_ := $18000000 + pos_pin & $3F
	ctrb_ += $04000000 + (neg_pin & $3F) << 9

PropellerマニュアルCRTBのところを見ると、PropellerのCog内のカウンタ/タイマ関係について、 以下のように詳細に書かれていた。 オーディオ出力はシリアルなのだから、絶対にカウンタ/タイマを使っている筈なのである。 まず、CRTA/CRTBのspin解説として

とあり、どんなふうに応用できるかとして、以下をズラリと並べている。

このプログラムでCRTBに設定しているデータのフォーマットは以下であった。

初期化ルーチンでCTRBに転送されるデータをstartメソッドで初期化しているところを確認すると、

	ctrb_ := $18000000 + pos_pin & $3F
	ctrb_ += $04000000 + (neg_pin & $3F) << 9

ということは、最上位1バイトについて言えば、0001 1000 + 0000 0100 = 0001 1100となる。 bit31は無視なので、bit30..26の5ビットのCTRMODEは00111である。 2バイト目はゼロなので、bit25.23の3ビットのPLLDIVは000である。 「pos_pin & $3F」の意味は、将来的な64ビットのPropellerのためにマスクしているだけなので、 これは単にpos_pinであり、これがbit5..0に置かれる。 neg_pinは同様に9ビット左シフトした位置のbit14..9に置かれる。 それぞれのピンの意味は以下である。

従って、以下のPLLDIVは000なので、「VCO / 128」が出力されることになる。

CTRMODEについての解説は以下であり

ここでのCTRMODEは00111ということで、以下の表から「DUTY differential」モードであることが判明した。 posとnegの2本のステレオ出力となっているが、この記述によれば、論理的に反転しているだけのように見える。 そこで、ステレオで鳴らしていたサウンドの、片方のチャンネルの信号をひっくり返して(交換して)みたところ、 ステレオの中央にびったりと定位した。 元のサウンドは、なんとなくステレオっぽく拡がっていたのだが、 これは「モノラル信号を片チャンネルから、その反転信号を反対チャンネルから出す」という、 まさに「擬似ステレオ」そのままであった。

ここでいよいよ、Webでダウンロードしていた このマニュアル の出番となった。 Propellerの各Cogsが持つ、強力であるというカウンタを理解する必要がでてきた。 その全体のブロック図は以下のようなものである。

ちょっと複雑なのはmulti-purposeであるからなので、今回のDUTY differentialモードの解説を見ると、 以下のようなブロックだけでいいことが判った。

このD/A出力は今後も色々に使うことが予想されるので、ここでマニュアルの説明をきちんと整理することにした。

Duty Cycle modes of operation

Modes %00110 (DUTY single-ended) and %00111 (DUTY differential) on the surface appear similar to the NCO/PWM modes of operation; however the waveforms they produce are very different. The output of the Duty Cycle modes is the carry output of the PHSA register, whenever the PHSA register overflows (wraps around from $FFFF_FFFF to $0000_0000) the APIN is set to 1. The block diagram for this mode of operation is shown in Figure 9. If FRQA is $0000_0001, the carry bit of PHSA will be 1 only once every 2^32 (4,294,967,296) cycles. At an 80MHz system clock, this will occur approximately once every 54 seconds. Similarly if FRQA is $FFFF_FFFF, the carry bit of PHSA will be 0 only once every 2^32 cycles.

以下が、上の解説に対応した出力例である。

そして、以下のように、オーディオ出力のD/Aに使う、という例の解説があった。

・・・これでだいぶ、先が見えてきた。 そこでまず、この段階でのexp016.spinとAudioOut03.spinを

と、VocalTrackと同様に動作するバージョンとして保管した。 そして、exp016.spinをコピーしてリネームしたexp017.spinを作り、 またAudioOut03.spinをコピーしてリネームしたAudioOut04.spinを作った。

そしてまず、音声合成のセグメントの演算で、 13種類のパラメータを次のセグメントに向けて時間的に補間していることが判ったので、 この部分をカットしてみた。 これによって、音声ではなくなって単なるフォルマント持続音が出ることになるが、 プログラムは13パラメータの数値演算とD/A出力だけになる。

上のようなMax/MSP側のパラメータ設定ソフトを作り、 対応して出来たPropellerソフト等は以下である。

これで、持続音に対して、音声合成の13パラメータをリアルタイムに個々に変化させることができた。 AudioOut04.spinの冒頭部分は、以下のようにスッキリした。
VAR
	long  cog, tract, zzzz4, zzzz5, attenuation, zzzz6    '6 longs       ...must              
	long  dira_, zzzz1, zzzz2, ctrb_, zzzz3, cnt_         '6 longs       ...be                   
	long  frames[4]                                       'many longs    ...contiguous                  

PUB start(parameters, pos_pin, neg_pin)
	dira_ |= (|< pos_pin + |< neg_pin)
	ctrb_ := $1C000000 + (pos_pin & $3F) + ((neg_pin & $3F) << 9)
	tract := parameters
	cnt_ := clkfreq / 20_000
	return cog := cognew(@entry, @attenuation)

PUB change
	bytemove(@frames[0] + 3, tract, 13)

DAT
entry
                        org
                        call    #initial
                        jmp     #loop
retadd                  long    :wait
:wait                   jmpret  retadd,#loop
                        mov     f_ptr,par
                        add     f_ptr,#4*8+3
                        movd    :set2,#par_curr   
                        mov     f_cnt,#13
:set                    jmpret  retadd,#loop
                        rdbyte  t1,f_ptr
                        shl     t1,#24
:set2                   mov     par_curr,t1
                        add     :set2,d0
                        add     f_ptr,#1
                        djnz    f_cnt,#:set
                        jmp     #:wait

' ##### Vocal Tract Job #####
loop                    waitcnt cnt_value,cnt_ticks     'wait for sample period
                        rdlong  t1,par                  'perform master attenuation
                        sar     x,t1
                        mov     t1,x                    'update duty cycle output for pin driving
                        add     t1,h80000000
                        mov     frqb,t1
                        mov     t1,par                  'update sample receiver in main memory
                        add     t1,#1*4
                        wrlong  x,t1
:White_noise_source     test    lfsr,lfsr_taps  wc      'iterate lfsr three times
                        rcl     lfsr,#1
・・・・・・・・
                        jmp     retadd                  'run segment of frame handler, return to loop

呼び出し側の親オブジェクトから、 13種類のパラメータのいずれかを変更した場合に呼ばれるメソッドをchangeとした。 これに対応して上のように、常時、13個のパラメータをPropeller共有RAMの領域からCog内に転送して、 24ビット左シフトしながら全て、内部演算用のpar_currに置いている。 ここは以前はセグメント補間の処理で複雑だったところである。

また、「Vocal Tract Job」の冒頭、 サンプリングクロックに対応した「waitcnt cnt_value,cnt_ticks」に続く以下の部分が、 カウンタに可変Dutyデータを書き込む、焦点の部分であることが明確になった。 なかなか見つからなかったが、ここにたった1箇所だけ、FRQBがあったのである。

                        rdlong  t1,par                  'perform master attenuation
                        sar     x,t1
                        mov     t1,x                    'update duty cycle output for pin driving
                        add     t1,h80000000
                        mov     frqb,t1
                        mov     t1,par                  'update sample receiver in main memory
                        add     t1,#1*4
                        wrlong  x,t1
この部分は、変数「x」が、カウンタに書き込まれるPWMデータとして前回の処理から受け継がれる。 まず、

                        rdlong  t1,par                  'perform master attenuation
                        sar     x,t1
                        mov     t1,x                    'update duty cycle output for pin driving
の部分では、「rdlong t1,par」で、音量制御(データをシフトして小さくする)パラメータをt1に格納し、 「sar x,t1」で、MSBのサインビットを残したまま、t1ビットだけデータを右シフトして小さくする。 これを再びt1に戻す。 ところが、既にmaster attenuationという概念は省略してしまったので、この3行のうち、 最初の2行は不要となり、カットした。 そして

                        mov     t1,x                    'update duty cycle output for pin driving
                        add     t1,h80000000
                        mov     frqb,t1
の部分、実はここしか残らないのであるが、 t1に$80000000を加算している。 これは「サインビットの反転」に他ならない。 PWMというのは、ディジタル的に反転する時間がサンプル値となるので、 このように、サンプリング周期ごとに符号を反転させ、それからの時間幅としてアナログ値を得るのであろう。 そして

                        mov     t1,par                  'update sample receiver in main memory
                        add     t1,#1*4
                        wrlong  x,t1
は、親メソッドから「現在のサンプルポイント」の問い合わせに返すための値なので、 これも省略した。 ・・・ということで、この肝心のD/A出力の部分はちょっと拍子抜けだが、
' ##### Vocal Tract Job #####
loop                    waitcnt cnt_value,cnt_ticks     'wait for sample period
                        mov     t1,x                    'update duty cycle output for pin driving
                        add     t1,h80000000
                        mov     frqb,t1
:White_noise_source     test    lfsr,lfsr_taps  wc      'iterate lfsr three times
                        rcl     lfsr,#1
・・・・・・・・
                        jmp     retadd                  'run segment of frame handler, return to loop

だけになってしまった。 これでいよいよ、このプログラムで解読していないのは、 音声合成アルゴリズムの数値演算とCORDICの部分だけとなった。 しかし、これはたぶん、理論をそのまま記述しているだけなので、 必要になったらいつでも確認できる。 ここで知りたかったのは「Propellerでのディジタルオーディオ」、 サウンドの出力の部分なのである。 そこで、サンプリング周期ごとの処理だけを残して、 まったくのオリジナルとして、同じメカニズムでサウンドを出力するプログラム、 というように、目標を修正することにした。

2008年3月27日(木)

Propellerマニュアルにあった「DUTY differentialモード」によるサウンド出力を実験するために、 まずexp017.spinをコピーしてリネームしたexp018.spinを作り、 またAudioOut04.spinをコピーしてリネームしたAudioOut05.spinを作った。 今度はAudioOut05.spinの大部分はカットされ、exp018.spinもほぼ全面改訂となる。 パラメータはMax/MSPからMIDI経由でいくらでも渡せるので、 Propellerの部分は最小限にする計画である。

まず、 exp018.spin の冒頭の部分は、以下のようにこれまでとまったく同じである。

{{ exp018.spin }}
CON
	_clkmode = xtal1 + pll16x
	_xinfreq = 5_000_000

OBJ
	Num : "Numbers"
	TV : "TV_Terminal"
	midiIn : "MidiIn03"
	midiOut : "MidiOut01"
	v : "AudioOut05"

そして、exp018.spinのMainの部分は以下のように、 とりあえずMIDIから7ビットのデータを1つだけPropellerに送るようにした。

PUB Main | dummy, para
	Num.Init
	TV.Start(12)

	TV.Str(string("Audio_output_Cog ="))
	dummy := v.start(10, 11)
	TV.Str(Num.ToStr(dummy, Num#DEC))   
	TV.Str(string(", MIDI_input_Cog ="))
	dummy := midiIn.start(7)
	TV.Str(Num.ToStr(dummy, Num#DEC)) 
	TV.Str(string(", MIDI_output_Cog ="))
	dummy := midiOut.start(6)

	TV.Str(Num.ToStr(dummy, Num#DEC)) 
	repeat
		dummy := midiIn.event
		if dummy <> -1
			midiOut.fifoset(dummy)
			TV.Str(Num.ToStr(dummy, Num#HEX7))
			if dummy == $903064
			elseif dummy == $903000
			elseif (dummy & $FFFF00) == $B00000
				para := dummy & $00007F
				v.change(para)
			elseif (dummy & $FFFF00) == $B00100

AudioOut05.spin の方は、まず冒頭のspinの部分は以下のように簡単にした。 累算値の初期値は、とりあえず$01としてみた。

VAR
	long  speed, dira_, ctrb_, cnt_

PUB start(pos_pin, neg_pin)
	dira_ |= (|< pos_pin + |< neg_pin)
	ctrb_ := $1C000000 + (pos_pin & $3F) + ((neg_pin & $3F) << 9)
	speed := $01
	cnt_ := clkfreq / 20_000
	return cognew(@entry, @speed)

PUB change(parameter)
	speed := parameter

パラメータは、親オブジェクトから「v.change(para)」と送ったものを、 changeメソッドで受けて、これを内部変数speedに格納している。 そしてアセンブラ領域は、以下で全てである。

DAT
entry
                        org
                        call    #initial
                        jmp     #loop
retadd                  long    :wait
:wait                   jmpret  retadd,#loop
                        jmp     #:wait

loop                    waitcnt cnt_value,cnt_ticks
                        mov     t1,x
                        add     t1,h80000000
                        mov     frqb,t1
                        mov     t2,par
                        rdlong  t1,t2
                        shl     t1,#24
                        add     x,t1
                        jmp     retadd

initial                 mov     reserves,#0
                        add     initial,d0
                        djnz    clear_cnt,#initial
                        mov     t1,par
                        add     t1,#4
                        rdlong  dira,t1
                        add     t1,#4
                        rdlong  ctrb,t1
                        add     t1,#4
                        rdlong  cnt_ticks,t1                        
                        mov     cnt_value,cnt
                        add     cnt_value,cnt_ticks
initial_ret             ret

h80000000               long    $80000000
d0                      	long    $00000200
clear_cnt               long    $1F0 - reserves
reserves
cnt_ticks               res     1
cnt_value               res     1
x                       res     1
t1                      res     1
t2                      res     1

Propellerツールでこのプログラムをコンパイルしてダウンロード・実行させると、 シンセサイザーで馴染みのある「鋸歯状波」のサウンドが鳴った。 MIDIからパラメータを変化させると、そのピッチが大幅に変化する。 これで、とりあえずのオーディオ出力のスタート地点に立ったことになる。

せっかくなので波形を見てみようと、数年ぶりにWindows95のノートを発掘して、 秋月電子で買ったオシロを繋いで、このアナログ出力波形を見てみることにした。 バッテリが寿命で再リセットとかが必要だったが、無事に動いて、 オシロのスクリーンショットをサーバに転送できた。 波形は以下のようなものである。

プローブが電圧を1/10にしているので、ちゃんとp-pで2.5Vぐらいは出ているようだ。 もう片方のチャンネルからは、もちろん極性反転した信号が出ていた。 以下が、この実験の様子である。 Propellerの小さなボードが、4台のパソコンに囲まれている風景である。

ここで見ている波形は、サンプリング周期ごとに、単純にパラメータを累算した$00000000から$FFFFFFFFまでの値が、 FRQBに書き込まれているので、このような鋸歯状波となるわけである。 Propellerの内部ROMには、サインテーブルが格納されているというので、 次の目標は、ピッチを変えて、さらに音量を変えて、サイン波をオーディオ出力することにした。

まず音量については、VocalTract.spinの中に、attenuationの処理があったのを参考にして、 0倍、1/2倍、1/4倍、・・・と7ビット(1/128倍まで)の単純シフトとする事にした。 送りのexp018.spinのMainの該当部分を以下のように、 コントロールチンジの次の番号で0-7の値をPropellerに送るようにした。

  repeat
    dummy := midiIn.event
    if dummy <> -1
      midiOut.fifoset(dummy)
      TV.Str(Num.ToStr(dummy, Num#HEX7))
      if dummy == $903064
      elseif dummy == $903000
      elseif (dummy & $FFFF00) == $B00000
        para := dummy & $00007F
        v.change1(para)
      elseif (dummy & $FFFF00) == $B00100
        para := dummy & $000007
        v.change2(para)

受けるAudioOut05.spinの方は、変数volumeを新設して、

VAR
	long  speed, dira_, ctrb_, cnt_, volume

PUB start(pos_pin, neg_pin)
	dira_ |= (|< pos_pin + |< neg_pin)
	ctrb_ := $1C000000 + (pos_pin & $3F) + ((neg_pin & $3F) << 9)
	speed := $01
	volume := 0
	cnt_ := clkfreq / 44_100
	return cognew(@entry, @speed)

PUB change1(parameter)
	speed := parameter

PUB change2(parameter)
	volume := parameter & %0111

DAT
entry
                        org
                        call    #initial
                        jmp     #loop
retadd                  long    :wait
:wait                   jmpret  retadd,#loop
                        jmp     #:wait

loop                    waitcnt cnt_value,cnt_ticks

                        mov     y,x
                        mov     t1,par
                        add     t1,#4*4
                        rdlong  t2,t1
                        and     t2,#%0111
                        sar     x,t2
                        add     x,h80000000
                        mov     frqb,x
                        mov     x,y
                        mov     t2,par
                        rdlong  t1,t2
                        shl     t1,#24
                        add     x,t1
                        jmp     retadd

と、累算値xをいったんyに格納して、D/A出力するところでだけ、 SAR命令によって、渡されたビット数だけ右シフトした。 この命令はSHR命令と違って、MSBの符号ビットはそのままにしてくれるので、 これだけで簡単にアッテネータの部分が実現できた。

Propellerのサインテーブルは当然のことながら1/4周期分しか格納されていないので、 位相によって読み出しと極性を調整する必要がある。 ところでVocalTract.spinの中では、「sine」という部分で以下のようにPropeller内部のテーブルを参照していて、 どうも位相と極性も調整しているっぽいので、この部分を真似てみることにした。

sine              	shr     t2,#32-13               	'get 13-bit angle
                        test    t2,h00001000    wz      	'get sine quadrant 3|4 into nz
                        test    t2,h00000800    wc      	'get sine quadrant 2|4 into c
                        negc    t2,t2                   		'if sine quadrant 2|4, negate table offset
                        or      t2,h00007000            	'insert sine table base address >> 1
                        shl     t2,#1                   		'shift left to get final word address
                        rdword  t2,t2                   		'read sine word from table
                        negnz   t2,t2                   		'if quadrant 3|4, negate word
                        shl     t2,#15                  		'msb-justify result

Propellerマニュアルによれば、メインメモリ中のサインテーブルは以下のように2049「ワード」、 すなわち16ビット幅である。 アドレスは$E000-$F001である。 1/4周期なので、最初のゼロ値から1/4周期で、終端の最大値まで持つために端数が出るわけである。

上のルーチンでは、位相t2はフルスケール13ビットであり、

sine              	shr     t2,#32-13               	'get 13-bit angle

で位相累算値t2を13ビットのフルスケールに収めて、

                        test    t2,h00001000    wz      	'get sine quadrant 3|4 into nz

によってそのMSBを見て、後半の周期(3/4 四半期)であればZフラグを立て、

                        test    t2,h00000800    wc      	'get sine quadrant 2|4 into c

によって、2/4 四半期であればCフラグを立て、

                        negc    t2,t2                   		'if sine quadrant 2|4, negate table offset

によって、このキャリーでt2を反転することで逆位相で読み出す。そして

                        or      t2,h00007000            	'insert sine table base address >> 1
                        shl     t2,#1                   		'shift left to get final word address
                        rdword  t2,t2                   		'read sine word from table

によってテーブルのアドレスオフセットに調整して、 データは1ワードなのでアドレスを2倍してから実際のサインテーブル値をt2に読み込み、

                        negnz   t2,t2                   		'if quadrant 3|4, negate word
                        shl     t2,#15                  		'msb-justify result

によって最後にzフラグで極性を反転し、 結果を32ビットのlong長に戻している。

この部分を自前のプログラムに組み込んでしばし実験して、 とりあえずサインが出る、という以下のプログラムが出来た。

波形は以下のように、綺麗なサイン波となった。

44.1KHzに変更したサンプリング周期ごとに処理しているのは、以下のような簡単な仕事である。

  • サンプル値xをyに保管
  • アッテネータ値で出力xをレベルダウン
  • xをFRQBに書き込むことでPWMオーディオ出力
  • 波形読み出しの位相累算値phaseを累算
  • これでサインテーブルを読み出す
  • 保管していたサンプル値yをxに戻す
  • サインの値をxに加算
以下が、プログラム中の該当する部分である。
loop                    waitcnt cnt_value,cnt_ticks
                        mov     y,x
                        mov     t1,par
                        add     t1,#4*4
                        rdlong  t2,t1
                        and     t2,#%0111
                        sar     x,t2
                        add     x,h80000000
                        mov     frqb,x
                        mov     t1,par
                        rdlong  t2,t1
                        add     phase,t2
                        mov     t2,phase
                        shr     t2,#32-13               'get 13-bit angle
                        test    t2,h00001000    wz      'get sine quadrant 3|4 into nz
                        test    t2,h00000800    wc      'get sine quadrant 2|4 into c
                        negc    t2,t2                   'if sine quadrant 2|4, negate table offset
                        or      t2,h00007000            'insert sine table base address >> 1
                        shl     t2,#1                   'shift left to get final word address
                        rdword  t1,t2                   'read sine word from table
                        negnz   t1,t1                   'if quadrant 3|4, negate word
                        shl     t1,#7
                        mov     x,y
                        add     x,t1
                        jmp     retadd

これでとりあえず「サイン」が出たが、まだ「オシレータ」と呼べるものではない。 実際には、Propellerのピン出力にRCフィルタが入っているので、 周波数特性としてはかなりのLPFがかかって、周波数ごとにレベルはかなり変化する。 これは仕方ないとして、もう少し、オシレータになるような改良を加えていこう。

2008年3月28日(金)

サイン波が出たということは、波形テーブルを参照すれば、周期波形については、あとはどんな信号も生成できる。 サンプリングをCDと同じ44.1KHzにしたこともあり、ここで波形のピッチの部分をきちんと整理しよう。

かつて 作るサウンドエレクトロニクス で、AKI-H8で同じように信号生成からMIDI楽器にまでシステムを進化させた時には、 12等分平均率に基づくMIDI対応のピッチごとに、AKI-H8のROMにデータテーブルを持った。 しかしPropellerでは、共有ROMもCog内RAMも容量に限りがあるので、 そのような潤沢なデータテーブルを無駄に確保するような戦略は採用できない。

その一方で、クロック80MHzのCogの処理能力はかなりのものなので、 同じMIDIの演奏情報の時間的トラフィックに対して、おそらくソフトウェア的になんとかしても、 そこそこは間に合う筈である。 経験的な直感から、ここでは1オクターブ分の位相累算値をテーブルとして持ち、 MIDIノートナンバに対応したピッチの処理は、リアルタイムにTopオブジェクトのCogが行うことにした。 まずは机上の設計としてデータを検討し、本当にそのピッチでサウンドが鳴るか、検証してみよう。

位相累算値のデータが半分になれば、波形のアクセスの時間が2倍かかって、 生成されるサウンドのピッチはオクターブ下がる。 そこで、最高音域の1オクターブ分の12個の位相累算値データがあれば、 それより低い場合には、オクターブ分だけデータを右ビットシフトすればよい。 1個のデータから平均率の12音を生成するには、「12乗根の2」の高精度乗除演算が必要なので、 これは採用せず、12個を愚直に持つことにする。 MIDIについて から、

-------------------------------------------------------
Octave||                     Note Numbers
   #  || C   | C#  | D   | D#  | E   | F   | F#  | G   | G#  | A   | A#  | B
-------------------------------------------------------
   0  ||   0 |   1 |   2 |   3 |   4 |   5 |   6 |   7 |   8 |   9 |  10 |  11
   1  ||  12 |  13 |  14 |  15 |  16 |  17 |  18 |  19 |  20 |  21 |  22 |  23
   2  ||  24 |  25 |  26 |  27 |  28 |  29 |  30 |  31 |  32 |  33 |  34 |  35
   3  ||  36 |  37 |  38 |  39 |  40 |  41 |  42 |  43 |  44 |  45 |  46 |  47
   4  ||  48 |  49 |  50 |  51 |  52 |  53 |  54 |  55 |  56 |  57 |  58 |  59
   5  ||  60 |  61 |  62 |  63 |  64 |  65 |  66 |  67 |  68 |  69 |  70 |  71
   6  ||  72 |  73 |  74 |  75 |  76 |  77 |  78 |  79 |  80 |  81 |  82 |  83
   7  ||  84 |  85 |  86 |  87 |  88 |  89 |  90 |  91 |  92 |  93 |  94 |  95
   8  ||  96 |  97 |  98 |  99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107
   9  || 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119
  10  || 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
-------------------------------------------------------

とあるので、MIDIノートナンバの最高音域では、 「Note 116 = G#9」から「Note 127 = G10」までを持てばよい。 ピッチの基準は「A4 = 440Hz」を使うことにして、ここでは簡単のために、 MIDI規約には存在しないが、形式的にあと全音だけ上にNoteを129まで延長して、 「Note 129 = A10」をまず計算する。 これはA4の6オクターブ上なので、ぴったり440 * 64 = 28160Hzである。

サンプリング周波数が44.1KHzということで、その1周期の時間は22675.736μsecほどである。 A10の28.160KHzの周期は35511.363μsecほどである。 A10のピッチはサンプリング定理の22.05KHzを越えているが、 累算値のデータは小数点以下の端数なので、理論的には計算できる。 求めるA10のための累算値データを x とすれば、32ビット(1 long)フル、 つまり$100000000を累算していると44.1KHzになるのに対して、 データ x を同じサンプリングで累算すると28.160KHzになるので、 比例関係から

$100000000 : 44.1 = x : 28.160

となり、以下のGoogle電卓の計算によって、「x = 2742546010」という整数値を得た。

この数値を元に、同様にGoogle電卓で「12乗根の2」を y として計算すると、

y = 2^(1 / 12) = 1.05946309

となるので、MIDI最高音のG10の場合には、同様にGoogle電卓で「x / y / y」を計算して、

G10 : (2742546010 / 1.05946309) / 1.05946309 = 2443330740

となった。 ここからは、出て来た結果をさらにyで割れば半音下のデータとなるので、 同様にGoogle電卓で計算して、以下を得た。

F#10 : 2443330740 / 1.05946309 = 2306197130

F10 : 2306197130 / 1.05946309 = 2176760240

E10 : 2176760240 / 1.05946309 = 2054588080

Eb10 : 2054588080 / 1.05946309 = 1939272920

D10 : 1939272920 / 1.05946309 = 1830429900

C#10 : 1830429900 / 1.05946309 = 1727695770

C10 : 1727695770 / 1.05946309 = 1630727660

B9 : 1630727660 / 1.05946309 = 1539201960

Bb9 : 1539201960 / 1.05946309 = 1452813200

A9 : 1452813200 / 1.05946309 = 1371273070

Ab9 : 1371273070 / 1.05946309 = 1294309430

ちなみに確認のために、最後のデータをさらにyで割って2倍してみると

(G9) : 1294309430 / 1.05946309 * 2 = 2434330860

となり、これはG10の2443330740とかなり近いもので、誤差の蓄積は心配ないオーダである。 このデータを親オブジェクトの側に定数データとして持ち、 MIDIノートナンバに対応してオクターブをシフトして、 まったく変更していないAudioOut06.spinに対してspeedパラメータとして渡すようにしたのが、 以下の「exp020.spin」プログラムである。

{{ exp020.spin }}

CON
	_clkmode = xtal1 + pll16x
	_xinfreq = 5_000_000

DAT
  f_number	long      1294309430,1371273070,1452813200,1539201960
              		long      1630727660,1727695770,1830429900,1939272920
              		long      2054588080,2176760240,2306197130,2443330740        

OBJ
	Num : "Numbers"
	TV : "TV_Terminal"
	midiIn : "MidiIn03"
	midiOut : "MidiOut01"
	v : "AudioOut06"

PUB Main | dummy, para, p1
	Num.Init
	TV.Start(12)
 
	TV.Str(string("Audio_output_Cog ="))
	dummy := v.start(10, 11)
	TV.Str(Num.ToStr(dummy, Num#DEC))   
	TV.Str(string(", MIDI_input_Cog ="))
	dummy := midiIn.start(7)
	TV.Str(Num.ToStr(dummy, Num#DEC))
	TV.Str(string(", MIDI_output_Cog ="))
	dummy := midiOut.start(6)
	TV.Str(Num.ToStr(dummy, Num#DEC))
 
	repeat
		dummy := midiIn.event
		if dummy <> -1
			midiOut.fifoset(dummy)
			TV.Str(Num.ToStr(dummy, Num#HEX7))
			if (dummy & $FF00FF) == $900064
				v.change2(0)
				p1 := (dummy & $007F00) >> 8
				para := f_number[ (p1+4) // 12 ] >> ( (127-p1) / 12 )
				v.change1(para)
			elseif (dummy & $FF00FF) == $900000
				v.change2(7)
			elseif (dummy & $FFFF00) == $B00000
				para := dummy & $000007
				v.change2(para)

このプログラムは、 以下のように作ったMax/MSPパッチ「MIDItest04.mxt」 によって、Mac側のソフトシンセのピアノ音と、Propellerの生成音とでユニゾンの確認ができた。 MIDIノートオンで「アッテネータ = 0」で発音し、MIDIノートオフで「アッテネータ = 7」として、 擬似的に消音しているだけの簡単なものである。

プログラムの中でちょっとトリッキーなのは

para := f_number[ (p1+4) // 12 ] >> ( (127-p1) / 12 )

であるが、オクターブ分、12個並べた配列のインデックスの部分には、 12個ごとに繰り返すので、剰余演算子「//」を使った。 普通は「%」を使うが、Propellerではバイナリ数値のために使うので、 ここはちょっとした注意点である。 あとは、オクターブが低いほどたくさん右シフトしているだけであり、 ここでは普通の除算(余りは捨てる)「/」を使っている。

これで、このプログラムの走るPropellerボードに、 電子ピアノなどをMIDIで繋いで、簡単な楽器としてスタンドアロンで使う・・・ というようなアプリケーションはかなり身近になった。 当面、あまりPropellerでこちらの方面に深入りするつもりはないので、 また鉾先を変えていくことにしよう。

2008年3月30日(日)

Propellerで目指すところに関係して、 このページ を改訂した。 これまでのインスタレーションは、基本的に
  • 外界からの入力を検出するセンサ
  • センサからホストへのインターフェース
  • ホストPC(Max/MSP/jitter, Flash, Processing等)
  • ホストから(広義)ディスプレイへのインターフェース
  • 外界に反応を返す(広義の)ディスプレイ
というシステム構成をしていた。 AKI-H8で作ったインターフェース、I-Cubeなど、 そしてGAINERも、この第2/第4項目のインターフェースである。 Propellerについても、ここまでの実験で、この領域に十分に参加できる可能性は確信した。

しかしさらに、もしインターフェースの部分の情報処理能力が高いのであれば、 第3項目まで含めて、つまりパソコンを不要としてシステムを構成できる。 Propellerには、この可能性を強く感じる。 このページ のインスタレーションの中にはごく少数、その発想でAKI-H8だけで実現したものもあるが、 まだまだホストPCは全盛である。 とりあえずAKI-H8やGAINERやBasicStampやPICに加わることから始めるとして、 目標はそこで終わらず、より高いところを目指したい。

さて、明日から2日間は学科会議合宿でPropellerと遊ぶことも出来ないので、 今日はちょっとだけの改造をすることにした。 これまで作ってきたPropellerソフトウェアでは、 ビデオ出力の部分は、Parallax社の提供したオブジェクトをそのまま、 ソフトウェア部品として利用してきた。 しかし、せっかくなら不要な部分を削ったり、機能を追加してオリジナル化してみたい。 そこで、OBJモジュールで参照されている

  • Numbers.spin
  • TV_Terminal.spin
  • TV.spin
  • Graphics.spin
について、コピーしてオリジナル名としたものをカレントディレクトリに置いて、 実験することにした。後ろの2つは、「TV_Terminal.spin」から参照されているものである。

親オブジェクトの方は、exp020.spinをコピーしてexp021.spinとリネームし、 まずは「MIDIを受けてサイン波形を生成する」というSPECは変更せず、 子オブジェクト・孫オブジェクトの改訂だけを行うことにした。 対象となるのは、

  • E_Numbers01.spin
  • E_TV_Terminal01.spin
  • E_TV01.spin
  • E_Graphics01.spin
である。 まずは、簡単に完結していそうな「E_Numbers01.spin」を調べた。

最初に、各オブジェクトのソースをリネームしただけの全プログラムをコンパイルして、 「Object Info」によって、以下のメモリ使用量のスクリーンショットを撮った。 不要な機能を削減したとして、どのくらい「効く」のかをテストするためである。

「E_Numbers01.spin」は、テキスト/ストリングの処理メソッドをごっそり集めたオブジェクトである。 しかし、基本的にビデオ出力のデバッグ画面に、10進あるいは16進でデータを表示する以外の機能については、 それほど使う予定もないので、カットすることにした。 削除したメソッドは以下の2つである。

  • PUB FromStr(StrAddr, Format): Num | Idx, N, Val, Char, Base, GChar, IChar, Field
  • PRI InBaseRange(Char, Base): Value
さらに、使用しないモードの表示フォーマット用データをばっさりと切ってみたが、 以下のように、プログラムが56longs減っただけで、ほとんど影響が無かった。

続いて、親オブジェクトから呼ばれているもう一つの「TV_Terminal.spin」を調べた。 この中には、テキスト表示のメソッドと、10進、16進、バイナリ、の3種類で数値を表示するメソッドがあったが、 数値表示はE_Numbers01.spinのNum.ToStrメソッドを使ってテキスト表示しているので、 数値表示の部分をカットすることにした。 削除したメソッドは、ビデオ表示のstopを入れて以下の4つである。
  • PUB stop
  • PUB dec(value) | i
  • PUB hex(value, digits)
  • PUB bin(value, digits)
しかし以下のように、プログラムが37longs減っただけで、ほとんど影響が無かった。

RAMに広大なグラフィックメモリを確保しているのもここであるが、 とりあえずまずはここは手を付けていない。

続いて、「E_TV01.spin」を調べた。 トップオブジェクトの初期化では、MIDIの入出力と、オーディオについては、 初期化の際にCog番号が返ってきて、それをビデオ表示した。 ところが、ビデオだけは「TV_Terminal.spin」の中で単に「TV.Start」を呼ぶだけで、 何も戻ってこなかった。 これは、そのさらに下の(子の子で孫メソッド)、「E_TV01.spin」の中のstartメソッドを呼んでいたからである。 そこで、この孫メソッドからの返り値としてCog番号を受け取り、 さらに子メソッドとして返すようにして、トップで無事に全ての初期化のCog番号が表示された。

「E_TV01.spin」は機能として、ビデオ出力の「NTSC」「PAL」の両方に対応している。 まずPALを使うことはないのでここをカットしようとしたが、 かなりトリッキーに両方に対応したコードとなっていたので、これは断念した。 Propellerの設計思想として、「NTSC用」「PAL用」などと冗長に両方のプログラムを用意して、 ビデオモードに対応してそのいずれかにジャンプする、などというのはダサイのだろう。 まだまだ、プログラムROMが潤沢にある旧来型のマイコンの発想だったことを反省した。

続いて、「E_Graphics01.spin」を調べた。 これは凄いオブジェクトであり、専用に1つのCogを使って、グラフィックドライバを構成していた。 「TV_Terminal.spin」からは、上記のtv.startと、こちらのためのgr.startの2つのNEWCOGを発令していた。 そこで、この両方から起動Cog番号を返して、 TV_Terminal.spinでは片方を10倍して返して、トップオブジェクトでデコードして、両方を表示するようにした。 カットしたのはstopメソッドぐらいなので、以下のようにちょっとサイズが増えた。

ここまでで、システムを構成しているPropellerソースは以下の8本である。 Cog IDやMIDI入力の情報をリアルタイムにビデオ出力し、 MIDIを送受信し、そのMIDIノートナンバに対応したピッチのサイン波形をオーディオ出力する、 という仕事を、6個のCogsによって実行している。 時分割でも割り込みでもなく各Cogsは自分の仕事を並列処理するシステムである。 これによって、以下のように、ビデオモニタからは、メイン(Cog ID=0)以外の、 全てのCogsの番号が表示されるようにできた。

グラフィックドライバ・オブジェクトの「E_Graphics01.spin」を眺めてみると、 かなり強力なグラフィックメソッドを完備していることが判明した。 これは、ビデオ・インスタレーションにはもってこいである。 ただし現状では、これはトップから見て孫メソッドなので、いちいち「TV_Terminal.spin」を介して制御する必要がある。 初期設定のCog番号を返す方法も、これが原因でちょっとダサいものになっている。 そこで、「E_TV01.spin」と「E_Graphics01.spin」を、 「TV_Terminal.spin」と同じ階層に並べられないか、実験してみた。 これが可能となれば、トップオブジェクトから直接に、グラフィックドライバのメソッドを利用できる筈である。

そして、メインとグラフィクス関係の3つ、の計4本のソースをコピーしてリネームし、 同じ動作を行うかチェックしながら改造した結果、結論として「出来ない」ことが判明した。 具体的には、まず親オブジェクト「exp022.spin」の該当部分は以下である。

{{ exp022.spin }}

・・・・・・・・

OBJ
	midiIn : "MidiIn03"
	midiOut : "MidiOut01"
	audio : "AudioOut06"
	Num : "E_Numbers02"
	TV : "E_TV_Terminal02"
	gr : "E_Graphics02"

PUB Main | dummy, para, p1
	Num.Init                      { Num.Init MUST be called before first object use. }
	para := TV.Start1(12)
	dummy := TV.Start2
	TV.out(0)                     { display home }
	TV.Str(string("TV_port_Cog ="))
	TV.Str(Num.ToStr(para, Num#DEC))
	TV.Str(string(", Graphic_driver_Cog ="))
	TV.Str(Num.ToStr(dummy, Num#DEC))
・・・・・・・・

	repeat
		dummy := midiIn.event
		if dummy <> -1
			midiOut.fifoset(dummy)
			TV.Str(Num.ToStr(dummy, Num#HEX7))
		if (dummy & $FF00FF) == $900064
・・・・・・・・
		elseif (dummy & $FFFF00) == $B00100
			para := dummy & $000003
			TV.gr_test(para)
'			gr.color(para+1)

ここで、子オブジェクト「E_TV_Terminal01.spin」の該当部分は以下である。

OBJ
	tv    : "E_TV02"
	gr    : "E_Graphics02"

PUB gr_test(para)
	gr.color(para+1)

PUB start1(basepin) : status
''  basepin = first of three pins on a 4-pin boundary (0, 4, 8...) to have
''  1.1k, 560, and 270 ohm resistors connected and summed to form the 1V,
''  75 ohm DAC for baseband video  
	bitmap_base := (@bitmap + $3F) & $7FC0
	repeat x from 0 to x_tiles - 1
		repeat y from 0 to y_tiles - 1
			screen[y * x_tiles + x] := bitmap_base >> 6 + y + x * y_tiles
	tvparams_pins := (basepin & $38) << 1 | (basepin & 4 == 4) & %0101
	longmove(@tv_status, @tvparams, paramcount)
	tv_screen := @screen
	tv_colors := @color_schemes
	status := tv.start(@tv_status)

PUB start2 : status
	status := gr.start
	gr.setup(x_tiles, y_tiles, 0, y_screen, bitmap_base)
	gr.textmode(x_scale, y_scale, x_spacing, 0)
	gr.width(width)

また、孫オブジェクト「E_Graphics02.spin」の該当部分は以下である。

PUB color(c)
'' Set pixel color to two-bit pattern
''   c              - color code in bits[1..0]
	setcommand(_color, @colors[c & 3])                    'set color
親オブジェクトの「exp022.spin」では、OBJブロックで、子オブジェクトだけでなく、 孫オブジェクトも(強制的に)定義している。 そして、このプログラムを実行すると、MIDIコントロールチェンジ0番のデータとして、 子メソッド「TV.gr_test(para)」にパラメータpara(0-3) が渡される。 これは孫メソッド「color(c)」にパラメータcとして渡されるので、 システムとしての動作はこれまでと同じながら、以下のように表示の色だけが変わった。 これは予想される正しい動作である。

ところで、上記のプログラムで親オブジェクトの最後の行で「'」でコメントアウトしている、 「gr.color(para+1)」のコメントを外して、続けて実行してみると、ビデオ出力がまったく動かなくなった。 MIDI入出力とサウンド生成はそのまま動いているのに、ビデオ表示が止まってしまうのである。 これは、孫オブジェクトに強制的にメッセージを送ったのが、どこかのパラメータを異常に書き換えたためと思われる。 結局、親・子・孫という3階層を2階層にする、という試みは、このままで簡単には実現しない事が判った。 もちろん、「E_TV_Terminal01.spin」の内容を全て親に移動させれば出来るのは当然だが、 それではトップがかなり煩雑になるので、それも避けたい。

・・・結論として、グラフィックドライバは「E_TV_Terminal01.spin」を経由して叩く、ということにした。 それでも、これまで完全なブラックボックスだったライブラリの中を、 ざっと眺めただけとはいえ、オリジナル・オブジェクト化したのは、今後に向けては収穫である。 ビデオドライバのPropellerアセンブラでは、どこかで見たようなジャンプテーブルがあったし、 グラフィックドライバのPropellerアセンブラでは、どこかで見たような座標計算のための数値演算があった。 当然のことだが、これらのソフトウェア部品を有効に使い回すことで、 Propellerのライブラリは効率良く、そして強固に出来上がっているのだった。

2008年4月1日(火)

ちょっとだけPropellerが見えてきた数日前から、 「トランジスタ技術」の編集部の人とやりとりしていたが、 およそ以下のようなことで記事を書く、ということになった。 CQ出版に原稿を書くのはだいぶ久しぶりのことだなぁ。
		*記事概要
			テーマ:Propellerを使ったxxxxの製作
			予定ページ数:8ページ程度
			予定掲載号:トランジスタ技術 2008年8月号
			原稿締め切り日:6/15(要相談)
			文字数(目安):約10000字程度
			図表点数(目安):12点程度

こうなると、いよいよ何かターゲットを決めて、具体的に作っていくことになる。 ちょっと燃えてきたぞ。

手元には40ピンDIPのPropellerチップが10個あるだけなので、 さっそく、ネット経由で部品を注文した。 ところが、シリアルEEPROMの 24LC256 については、この商社は、8ピンDIPを取り扱っていなくて、以下のMSOPというパッケージしかない。

ピン間は0.65ミリである。DIPの0.1インチの2.54mmとはだいぶ違う。 とりあえず、変換基板とレジストも注文してみた。 果たして、うまくハンダ付けできるだろうか。

2008年4月2日(水)

昨日の夕方、17:55にネットで注文したのに、もう今日の昼過ぎには部品が届いた。 さすが「電子部品のアスクル」、ちょっと高いけれども便利な商社である。

 

さて問題は、シリアルEEPROMの0.65ミリというMSOPパッケージと、それをDIPに変換する変換基板である。

 

出して並べてみると・・・これは小さい(^_^;)。 鼻息で吹き飛んで床に落としたら、もう絶対に発見できそうもない。 これを付けられるだろうか。

 

とりあえず、道具はいつものハンダ一式。 ただし必殺のツール、「フラックス」だけが頼りである。

そして、息を止めて集中すること数分間。 さすがにフラックスは素晴らしく、最初の挑戦からアッサリと綺麗にハンダ付け出来た(^_^)。 1枚の変換基板に多数のEEPROMを並べるのは次回にするとして、今回は変換基板をカットしてみた。 もちろん、残りの部分にもあと1個は取り付けできる。

 

さて、それでは製作を進めよう・・・と部品を並べていて、5MHzの水晶振動子を注文し忘れていたことに気付いた。 仕方ないのでネットで注文して、これ以降の製作は、水晶が届く明日にすることにした。

ハードを離れて、Propellerツールに向かい、あらためてグラフィクス関係のライブラリを捜してみると、

  • Graphics_Demo.spin
  • Mouse.spin
  • TV.spin
  • Graphics.spin
という一群の入ったディレクトリを発見した。 こういう場合には「demo」の名前がトップだ、ということで叩いてみると、 なんとビデオモニタにカラーグラフィックのアニメーションが現れた。 テキスト情報表示もいいけれど、これも欲しかったのである。 そして、PS2マウス対応だというMouse.spinはとりあえず不要であり、 TV.spinとGraphics.spinについては、ここまでのPropellerライブラリの傾向から、 おそらく既に調べたものと同一では、という気がした。 そこでとりあえず、
  • ここまでの最新のexp022.spinはそのままに保留しておく
  • Graphics_Demo.spinをコピーしてexp023.spinとリネームして改造
  • exp023.spinからの参照はE_TV02.spinとE_Graphics02.spinとするように変更
  • E_TV02.spinとE_Graphics02.spinは変更しない
  • マウス処理をカット/コメントアウトする
  • それでもグラフィックのデモが同様に行われる
となるように整理してみた。 これはかなり簡単な作業であり、やはりE_TV02.spinとE_Graphics02.spinは、 TV.spinとGraphics.spinにそのまま置換できた。 以下はそのムービーである。

そして、画面内では、文字列として定義した「Propeller」というテキストのグラフィック表示も実現していることから、 16進表示についてソフトウェア的に盛り込めば、exp022.spinから呼ばれていたE_TV_Terminal02.spinナシに、 直接にE_TV02.spinとE_Graphics02.spinを参照できることがわかった。

そこで、これまでに制作したMIDI関係とサウンド(サイン波)生成の機能のあるexp022.spinを、 ここでのexp023.spinに統合していくことを目指した。 MIDIの16進表示については、E_Numbers01.spinを使わないで表示することになる。

まずはMIDIの16進表示ナシで、単純に両者のプログラムを合体させたのが、 以下のexp024.spinである。 これで同様のグラフィックをライブ生成しながら、 MIDI入力・MIDI出力・オーディオ生成も同時に実行している。

{{ exp024.spin }}  

CON
  _clkmode = xtal1 + pll16x
  _xinfreq = 5_000_000
  _stack = ($3000 + $3000 + 100) >> 2   'accomodate display memory and stack
  x_tiles = 16
  y_tiles = 12
  paramcount = 14       
  bitmap_base = $2000
  display_base = $5000
  lines = 5
  thickness = 2

VAR
  long  tv_status     '0/1/2 = off/visible/invisible           read-only
  long  tv_enable     '0/? = off/on                            write-only
  long  tv_pins       '%ppmmm = pins                           write-only
  long  tv_mode       '%ccinp = chroma,interlace,ntsc/pal,swap write-only
  long  tv_screen     'pointer to screen (words)               write-only
  long  tv_colors     'pointer to colors (longs)               write-only               
  long  tv_hc         'horizontal cells                        write-only
  long  tv_vc         'vertical cells                          write-only
  long  tv_hx         'horizontal cell expansion               write-only
  long  tv_vx         'vertical cell expansion                 write-only
  long  tv_ho         'horizontal offset                       write-only
  long  tv_vo         'vertical offset                         write-only
  long  tv_broadcast  'broadcast frequency (Hz)                write-only
  long  tv_auralcog   'aural fm cog                            write-only
  word  screen[x_tiles * y_tiles]
  long  colors[64]
  byte  x[lines]
  byte  y[lines]
  byte  xs[lines]
  byte  ys[lines]

OBJ
  midiIn : "MidiIn03"
  midiOut : "MidiOut01"
  audio : "AudioOut06"
  tv : "E_TV02"
  gr : "E_Graphics02"

PUB start | i, j, k, kk, dx, dy, pp, pq, rr, numx, numchr, dummy, para, p1

  'start tv
  longmove(@tv_status, @tvparams, paramcount)
  tv_screen := @screen
  tv_colors := @colors
  tv.start(@tv_status)

  'init colors
  repeat i from 0 to 63
    colors[i] := $00001010 * (i+4) & $F + $2B060C02

  'init tile screen
  repeat dx from 0 to tv_hc - 1
    repeat dy from 0 to tv_vc - 1
      screen[dy * tv_hc + dx] := display_base >> 6 + dy + dx * tv_vc + ((dy & $3F) << 10)

  'init bouncing lines
  i := 1001
  j := 123123
  k := 8776434
  repeat i from 0 to lines - 1
    x[i] := ?j // 64
    y[i] := k? // 48
    repeat until xs[i] := k? ~> 29
    repeat until ys[i] := ?j ~> 29

  'start and setup graphics
  gr.start
  gr.setup(16, 12, 128, 96, bitmap_base)

  'start MIDI/audio
  midiIn.start(7)
  midiOut.start(6)
  audio.start(10, 11)

  repeat

    'clear bitmap
    gr.clear

    'draw spinning triangles
    gr.colorwidth(3,0)
    repeat i from 1 to 8
      gr.vec(0, 0, (k & $7F) << 3 + i << 5, k << 6 + i << 8, @vecdef)

    'draw expanding pixel halo
    gr.colorwidth(1,k)
    gr.arc(0,0,80,30,-k<<5,$2000/9,9,0)

    'step bouncing lines
    repeat i from 0 to lines - 1
      if ||~x[i] > 60
        -xs[i]
      if ||~y[i] > 40
        -ys[i]
      x[i] += xs[i]
      y[i] += ys[i]

    'draw bouncing lines
    gr.colorwidth(1,thickness)
    gr.plot(~x[0], ~y[0])
    repeat i from 1 to lines - 1
      gr.line(~x[i],~y[i])
    gr.line(~x[0], ~y[0])

    'draw spinning stars and revolving crosshairs and dogs
    gr.colorwidth(2,0)
    repeat i from 0 to 7
      gr.vecarc(80,50,30,30,-(i<<10+k<<6),$40,-(k<<7),@vecdef2)
      gr.pixarc(-80,-40,30,30,i<<10+k<<6,0,@pixdef2)
      gr.pixarc(-80,-40,20,20,-(i<<10+k<<6),0,@pixdef)

    'draw small box with text
    gr.colorwidth(1,14)
    gr.box(60,-80,60,16)
    gr.textmode(1,1,6,5)
    gr.colorwidth(2,0)
    gr.text(90,-72,@pchip)

    'draw incrementing digit
    if not ++numx & 7
      numchr++
    if numchr < "0" or numchr > "9"
      numchr := "0"
    gr.textmode(8,8,6,5)
    gr.colorwidth(1,8)
    gr.text(-90,50,@numchr)

    'copy bitmap to display
    gr.copy(display_base)

    'increment counter that makes everything change
    k++

    dummy := midiIn.event
    if dummy <> -1
      midiOut.fifoset(dummy)
      if (dummy & $FF00FF) == $900064
        audio.change2(0)
        p1 := (dummy & $007F00) >> 8
        para := f_number[ (p1+4) // 12 ] >> ( (127-p1) / 12 )
        audio.change1(para)
      elseif (dummy & $FF00FF) == $900000
        audio.change2(7)
      elseif (dummy & $FFFF00) == $B00000
        para := dummy & $000007
        audio.change2(para)
      elseif (dummy & $FFFF00) == $B00100
        para := dummy & $000003
    
DAT
  f_number    long      1294309430,1371273070,1452813200,1539201960
              long      1630727660,1727695770,1830429900,1939272920
              long      2054588080,2176760240,2306197130,2443330740        

tvparams                long    0               'status
                        long    1               'enable
                        long    %001_0101       'pins
                        long    %0000           'mode
                        long    0               'screen
                        long    0               'colors
                        long    x_tiles         'hc
                        long    y_tiles         'vc
                        long    10              'hx
                        long    1               'vx
                        long    0               'ho
                        long    0               'vo
                        long    0               'broadcast
                        long    0               'auralcog

vecdef                  word    $4000+$2000/3*0         'triangle
                        word    50
                        word    $8000+$2000/3*1+1
                        word    50
                        word    $8000+$2000/3*2-1
                        word    50
                        word    $8000+$2000/3*0
                        word    50
                        word    0

vecdef2                 word    $4000+$2000/12*0        'star
                        word    50
                        word    $8000+$2000/12*1
                        word    20
                        word    $8000+$2000/12*2
                        word    50
                        word    $8000+$2000/12*3
                        word    20
                        word    $8000+$2000/12*4
                        word    50
                        word    $8000+$2000/12*5
                        word    20
                        word    $8000+$2000/12*6
                        word    50
                        word    $8000+$2000/12*7
                        word    20
                        word    $8000+$2000/12*8
                        word    50
                        word    $8000+$2000/12*9
                        word    20
                        word    $8000+$2000/12*10
                        word    50
                        word    $8000+$2000/12*11
                        word    20
                        word    $8000+$2000/12*0
                        word    50
                        word    0

pixdef                  word                            'crosshair
                        byte    2,7,3,3
                        word    %%00333000,%%00000000
                        word    %%03020300,%%00000000
                        word    %%30020030,%%00000000
                        word    %%32222230,%%00000000
                        word    %%30020030,%%02000000
                        word    %%03020300,%%22200000
                        word    %%00333000,%%02000000

pixdef2                 word                            'dog
                        byte    1,4,0,3
                        word    %%20000022
                        word    %%02222222
                        word    %%02222200
                        word    %%02000200

pchip                   byte    "Propeller",0           'text

とりあえずここまで動けば、グラフィクスのパラメータをサウンドと同時に制御することで、 まさにマルチメディア・インスタレーションの入口に立ったことになる。 明日からが楽しみになってきた。

2008年4月3日(木)

新学期の準備に追われているうちに午後になり、昨日注文していた水晶も届いた。 とりあえず、オリジナルのPropeller回路が動くかどうかまでは、作って実験してみよう。 まずは電源である。 マルツの小型スイッチングACアダプタの「+5V 2A」を使って、 +5Vラインは3端子レギュレータでなく直接、この出力が使えないか、試してみることにした。 +5Vを受けて3.3Vにしているのは、LM3940である。 パスコンは、参考回路図には「1000μF」とあったが、たまたま手元に「1F」という強烈なのがあったので、 これを+5Vと+3.3Vのラインに入れて、積層セラミックコンデンサの105をここにパラってみた。

   

とりあえず電圧はちゃんと出たので、Propellerマニュアルにあった、 以下の回路図に従って、シリアルEEPROMとPropellerクリップを接続してみた。

いつもは小さく感じるリセットスイッチが大きく感じるのは不思議だが、こんなカンジになった。

   

   

ここでとりあえず、パソコンPropeller Toolに移動して、 これまで使っていたPropeller Demo BoardからUSBケーブルを外して、 この試作基板に接続されたPropellerクリップに繋いでみた。 すると、WindowsはこちらにのPropellerクリップも、自動的に新しいハードウェアとして認識した。 そしてPropeller Toolのハードウェア認識メニューを走らせると、 以下のように、これまでのCOM4から新しいCOM5となって、無事に認識した。

 

Propeller Toolから、これまでに作ったプログラムをPropellerのRAMに転送するのも成功、 そしてVerifyチェックまである「EEPROMに転送」の実行もエラーなく成功した。 無事に、昨日の微細ハンダ付けのシリアルEEPROMは稼動しているようである。 あとはMIDI、オーディオ、ビデオのハードを作ればいいが、ちょっとビデオについては検討の余地があるので、 今日はここまで、とした。 自分でハンダ付けしたハードウェアが動くというのは、出来合いのDemoボードが当然のように動くのとはまた違って、 嬉しいものだ。

2008年4月4日(金)

トラ技の記事の製作例にするシステムについて考えてみた。 編集部の希望として、
  • Propellerの特長(処理能力)活かした製作記事
  • 並列処理をハードウェアで行えることががPropellerの特長のひとつ
     →そのような処理対象を製作テーマにできれば
  • あまり肩肘張った記事では読者から敬遠される
  • 遊び心も多少含めて
というのがあった。 またこちらからのメイルの中に

若いエンジニアに「発想の転換CPU」「温故知新CPU」として紹介したい

と書いたところ、 「記事中にコラムを設けて,1000字程度で触れるか、本文中にさりげなく含めて」 と、こちらも了解をもらった。 これを受けて編集部に提案したのは、

  • 製作例として「インスタレーション」ということでいきたい
  • 科学館などで体験できる、「不思議なビデオアート装置」みたいなイメージ
というテーマである。 AKI-H8やGAINERを使って、ホストPCにMax/MSP/jitterやFLASHやprocessingを使えば、 かなりのことが出来るのは当然として、 ここでは是非、「パソコン無しでソコソコの仕事をする」システムとしてみたい。 Propellerの8つのCogsがそれぞれの仕事をテキパキとこなして、 外界との関係性のアルゴリズムをspinでサクサクと作れる、 Propeller Toolによって、現場でもデバッグや改訂が出来る、 というあたりをアピールするのが目標である。

まだ「マイク入力」のサンプルプログラムと回路の実験をしていないが、 アナログの入力には1つのCogと2本の入出力ピンが必要なのは分かっている。 センサの入力はアナログであればこれと同等だし、 クロックタイプのセンサであれば、MIDI入力と同様にソフトウェアでモニタするので、 入力の1ピンだけで十分である。

これらをまとめて、とりあえず現状でのシステム構成要素として、 Propellerシステムを構成する機能ブロックを以下のように整理してみた。 これらを取捨選択してシステムを設計する、ということになる。

  • メイン処理
    • Cog(0)を使用する
    • プログラム用に入出力ピン4本を使用する
       (P30/31 - ホスト、P28/29 - シリアルEEPROM)
    • ディジタル入力(スイッチ等)は直接にポートをモニタできる
    • ディジタル出力(LED/リレー等)は直接にポートをドライブできる
    • ビデオ出力のフレームレートごとの変化を担当
    • MIDI入力FIFOをモニタしてイベントがあれば判定処理
    • MIDI出力したい情報はMIDI出力FIFOに積んでおくだけ

  • MIDI入力
    • RS232Cなどシリアル入力でも同等
    • 1個のCogを使用する
    • 入力ピン1本を使用する
    • 外付回路はフォトカプラとTR1個
    • ソフトウェア的にMIDI信号をモニタして受信
    • MIDIイベントFIFOに積んでMainがモニタする

  • MIDI出力
    • RS232Cなどシリアル出力でも同等
    • 1個のCogを使用する
    • 出力ピン1本を使用する
    • 外付回路は74HC05を1個
    • ソフトウェア的にMIDI信号を上げ下げする
    • Mainが積んだMIDI送信FIFOから出力する

  • サウンド出力
    • 1個のCogを使用する
    • 出力ピン2本を使用する→ステレオ(逆相)出力
    • 外付回路はRCフィルタのみ
    • Cog内部カウンタをPWMモードで使用する
    • サンプリングは44.1KHz楽勝
    • ソフトウェアでサウンド生成(シンセサイズ)できる
    • 高精度のサイン波テーブル内蔵

  • ビデオ出力(NTSC)
    • 2個のCogを使用する(グラフィックドライバ/ビデオ信号スキャン)
    • 出力ピン3本を使用する
    • 出力ピンは [4n] [4n+1] [4n+2] の連続した3本であること
    • 外付回路は3本の抵抗のみ→ミックスしてビデオ信号化
    • ビデオチップもフレームメモリも不要で、ソフトウェアでビデオを生成
    • ビデオ生成の詳細の解析はまだ(^_^;)

  • アナログ入力
    • 1個のCogを使用する
    • 入力ピン2本を使用する
    • 外付回路はRCのみ、外部A/Dは不要
    • アナログ入力の詳細の解析はまだ(^_^;)
このような各要素に対して、Propeller全体としての制約条件は
  • Cogは全部で8個まで
  • 入出力ピンは全部で32本まで
ということになる。 これまでに制作したインスタレーションなどの例であれば、 AKI-H8やI-CubeやGAINERの置き換え用途としては
  • MainのCog(0)
  • MIDI入力
  • MIDI出力
  • アナログ入力(センサ用)
ということになる。 これだと、センサ入力として1つのCogで複数のチャンネルを管理するのを別にして、 アナログ入力は5種類、ないし6種類の異なったタイプ(サンプリング周期、データ形態、 ソフトウェア後処理、等)を混在できそうである。

AKI-H8やI-CubeやGAINERでは困難な、 「パソコン無しのスタンドアロン」「ビデオ出力」「ステレオオーディオ出力」の機能を盛り込むとなると、 Propellerシステムの選択肢は俄然、増えてくることになる。 ここでは、欲張った場合にはピン数やCog数の制約から、 1つのPropellerで実現する制限が出て来るだろう。 ただしこれは、他の組み込みマイコンではちょっと手が届かない、贅沢な制約である。 例えば、

  • MainのCog(0)
  • (MIDI入力)(センサ用/デバッグ用)
  • ビデオ出力[1]
  • ビデオ出力[2]
  • ディジタル入力(センサ用)
  • アナログ入力(センサ用)
  • ステレオオーディオ出力(リアルタイム・シンセサイザ)
というようなシステムであれば、Cogは8個全部、 そしてセンサ入力に多数を使用しても、ピン数はなんとかなりそうである。 これをパソコン無しに実現し、マイコンシステムで悩む「割り込み」「マルチタスクモニタ」等が不要、 ということであれば、まさにPropellerの実力は鮮明になるだろう。 そこで今回のシステムとしては、このあたりを目指すことにした。

システムの方針が決まったので、試作基板の製作を続けることにした。 最終的には使わないとしても、せっかくオリジナル外付回路を作ったので、 MIDI INとMIDI OUTも製作し、目玉としてビデオ出力回路を2チャンネル、搭載することにした。 同時に2系統の異なる映像信号を生成するシステムというのは、パソコンでもなかなか大変だが、 Propellerであればもう1つのCogを映像生成に指定するだけの筈である。 オーディオはヘッドホンアンプでなく、ステレオミニジャックからのライン出力である。

まず最初に、Propeller Demo Boardのブレッドボード領域に製作した、 以下のMIDI IN回路とMIDI OUT回路とを、この新しい試作基板の上に作ってみた。

基板上ではこんなものである。

 

まずここまで作ったところで、Propeller Demo Boardと取り替えて、動作確認した。 ポートのピンの位置については、オーディオも含めて、以下のように定義した。

  • P30/31 - ホスト接続用
  • P28/29 - シリアルEEPROM用
  • P27 - MIDI Input
  • P26 - MIDI Output
  • P24/25 - Audio出力(ステレオ)
これに合わせて、ビデオも含めてポート(ピン)番号の定義がこれまでのPropeller Demo Boardと違ってくるので、 Topオブジェクトのファイル名については、新しい試作基板のものをまったく新しいものに変更した。 該当する変更部分は以下となる。
OBJ
	midiIn : "MidiIn03"
	midiOut : "MidiOut01"
	v : "AudioOut06"

PUB Main | dummy, para, p1
	TV.Str(string("Audio_output_Cog ="))
	dummy := v.start(24,25)
	TV.Str(Num.ToStr(dummy, Num#DEC))   
	TV.Str(string(", MIDI_input_Cog ="))
	dummy := midiIn.start(27)
	TV.Str(Num.ToStr(dummy, Num#DEC))
	TV.Str(string(", MIDI_output_Cog ="))
	dummy := midiOut.start(26)
	TV.Str(Num.ToStr(dummy, Num#DEC))

そして、以下のように無事にここまでは動いた。 まだビデオ出力は無いので、Max/MSPからMIDIを出して、 それがPropellerのソフトスルーでMIDIとして戻ることで確認した。

 

これに続いて、オーディオと、2系統のビデオ出力の回路を増設した。 オーディオは、とりあえずPropellerの出力から4.7KΩで受けて、GNDへは0.1μF、出力へは10μFで出した。 マニュアルとかのドキュメントによって、だいぶ数値のばらつきがあり、抵抗では10KΩと220Ωの両方の記事があった。 ビデオのピンの位置については、以下のように定義した。 片方をPropeller Demo Boardと同じにして、他のポートを8ビット単位で空けておくためである。

  • P8/9/10 - ビデオ出力(A)
  • P12/13/14 - ビデオ出力(B) - Propeller Demo Boardと同じ
1.1KΩの抵抗が無かったので2.2KΩを並列にして、基板上ではこんなものである。

 

ビデオ出力の片方をPropeller Demo Boardと同じにしたので、 同じプログラムで、とりあえずビデオ出力(B)は簡単に出てくれた。

 

   

あとはビデオ出力の基準(base)ピン番号を変えればビデオ出力(A)が出る・・・と思ったところで、ひっかかった。 Topオブジェクトからは、初期化の時に、明示的にビデオ出力のピン番号を指定していないのである。 ここで、リネームしたTopプログラムの proto001.spin と、ビデオ出力のために参照している E_TV02.spin との中から、関係していそうな部分を調べてみた。

proto001.spinの中で関係しそうなところは、

VAR
	long  tv_status, tv_enable, tv_pins, tv_mode, tv_screen       
	long  tv_colors, tv_hc, tv_vc, tv_hx, tv_vx, tv_ho, tv_vo
	long  tv_broadcast, tv_auralcog
	word  screen[x_tiles * y_tiles]
	long  colors[64]
	byte  x[lines], y[lines], xs[lines], ys[lines]

PUB start | i, j, k, kk, dx, dy, pp, pq, rr, numx, numchr, dummy, para, p1, thickness
	'start tv
	longmove(@tv_status, @tvparams, paramcount)
	tv_screen := @screen
	tv_colors := @colors
	tv.start(@tv_status)
・・・・・・・・

DAT
・・・・・・・・
tvparams      long    0               'status
              long    1               'enable
              long    %001_0101       'pins
              long    %0000           'mode
              long    0               'screen
              long    0               'colors
              long    x_tiles         'hc
              long    y_tiles         'vc
              long    10              'hx
              long    1               'vx
              long    0               'ho
              long    0               'vo
              long    0               'broadcast
              long    0               'auralcog

あたりである。 初期化のtv.start(@tv_status)で渡すパラメータのポインタtvparamsには、 「pins」として「%001_0101」という謎の数値があった。 Propeller Demo BoardではP12-14がビデオ出力なので、 「12」すなわち「%1100」という文字列があれば、と期待したが、これは外れた。

そこでE_TV02.spinの中で関係しそうなところを調べると、

・・・・・・・・
''VAR                   'TV parameters - 14 contiguous longs
''  long  tv_status     '0/1/2 = off/invisible/visible              read-only
''  long  tv_enable     '0/non-0 = off/on                           write-only
''  long  tv_pins       '%pppmmmm = pin group, pin group mode       write-only
''  long  tv_mode       '%tccip = tile,chroma,interlace,ntsc/pal    write-only
''  long  tv_screen     'pointer to screen (words)                  write-only      
''  long  tv_colors     'pointer to colors (longs)                  write-only                            
''  long  tv_ht         'horizontal tiles                           write-only                            
''  long  tv_vt         'vertical tiles                             write-only                            
''  long  tv_hx         'horizontal tile expansion                  write-only                            
''  long  tv_vx         'vertical tile expansion                    write-only                            
''  long  tv_ho         'horizontal offset                          write-only                            
''  long  tv_vo         'vertical offset                            write-only                            
''  long  tv_broadcast  'broadcast frequency (Hz)                   write-only                            
''  long  tv_auralcog   'aural fm cog                               write-only                            
''The preceding VAR section may be copied into your code.        
''After setting variables, do start(@tv_status) to start driver.                                                             
''All parameters are reloaded each superframe, allowing you to make live
''changes. To minimize flicker, correlate changes with tv_status.
''Experimentation may be required to optimize some parameters.
''Parameter descriptions:
・・・・・・・・
''  tv_pins
''
''    bits 6..4 select pin group:
''      %000: pins 7..0
''      %001: pins 15..8
''      %010: pins 23..16
''      %011: pins 31..24
''      %100: pins 39..32
''      %101: pins 47..40
''      %110: pins 55..48
''      %111: pins 63..56
''
''    bits 3..0 select pin group mode:
''      %0000: %0000_0111    -                    baseband
''      %0001: %0000_0111    -                    broadcast
''      %0010: %0000_1111    -                    baseband + chroma
''      %0011: %0000_1111    -                    broadcast + aural
''      %0100: %0111_0000    broadcast            -
''      %0101: %0111_0000    baseband             -
''      %0110: %1111_0000    broadcast + aural    -
''      %0111: %1111_0000    baseband + chroma    -
''      %1000: %0111_0111    broadcast            baseband
''      %1001: %0111_0111    baseband             broadcast
''      %1010: %0111_1111    broadcast            baseband + chroma
''      %1011: %0111_1111    baseband             broadcast + aural
''      %1100: %1111_0111    broadcast + aural    baseband
''      %1101: %1111_0111    baseband + chroma    broadcast
''      %1110: %1111_1111    broadcast + aural    baseband + chroma
''      %1111: %1111_1111    baseband + chroma    broadcast + aural
''      -----------------------------------------------------------
''            active pins    top nibble           bottom nibble
''
''      the baseband signal nibble is arranged as:
''        bit 3: chroma signal for s-video (attach via 560-ohm resistor)
''        bits 2..0: baseband video (sum 270/560/1100-ohm resistors to form 75-ohm 1V signal)
''
''      the broadcast signal nibble is arranged as:
''        bit 3: aural subcarrier (sum 560-ohm resistor into network below)
''        bits 2..0: visual carrier (sum 270/560/1100-ohm resistors to form 75-ohm 1V signal)

これによれば、Propeller Demo BoardのP12-14では、 「%001_0101」というのは、ppp=%001、mmmm=%0101、ということになる。 確かに「pins 15..8」のグループなので、おそらくモードは「baseband」なのだろうと推定した。 さて問題は、ビデオ出力(A)では、同じ「pins 15..8」のグループで、上位4ビットでなくて、 下位41ビットに移動したいのである。 つまり、ppp=%001は同じ筈であり、mmmmをどこにするか、という事になる。

ここで、Propellerの データシート の中から、以下の部分を思い出した。

mmmmのビット3については、chroma signalやaural subcarrierのビットでここでは関係ないので、 同じグループの下位で「baseband」にするには、mmmm=0000であろう、と判断して、 proto001.spinのDAT領域の該当部分を「%001_0000」としてみた。 すると、一発でもう片方のビデオモニタから映像が出て、あっさり解決してしまった。 やはり、苦労してPropellerアセンブラのソースを読んできた甲斐があった。

ここで結論として、とりあえずproto001.spinのDAT領域の該当部分に設定する値は、

  • ビデオ出力(A) - P8..10 = [001_0000]
  • ビデオ出力(B) - P12..14=[001_0101]
ということになった。 ただし、これでは排他的にしかビデオ信号が出ない。 PropellerのCogとして2系統を並列動作させる必要がある。 ちょっとトライした感じでは、Propeller Toolから「Run Time Error」が出て、単純に タスクをダブルで呼び出す・・・というカンジでは行かないようである。 いよいよ、難しいところに入ってきたのだろうか。

Propeller日記(5) へ

「日記」シリーズ の記録