Fortran 2003/08 による mpeg audio layer 1 encoder
大昔に Fortran 90/95 で書いた mpeg-1 audio layer 1 のエンコーダを、Fortran 2003/08 の勉強のために書き直してみました。
おおむね Fortran 2003 ですが、微妙に Fortran 2008 の命令も使っています。簡単な
オブジェクト指向の機能を使ってみましたが、セッター・ゲッターを書くのがだるいので、限定的です。
コンパイラは intel fortran ver.17.02 を用いました。コンパイル時のオプションとして 「F2003セマンティクスを有効にする」必要があります。そうしないとコンパイルは出来ますが、実行時エラーが起きます。gfortran ではまだチェックしてません。(gfortran に無いParameterized derived types などの使用は避けました。)
gfortran ver.5.4.0 でも動作確認できました。
gfortran -std=f2008 kind.f90 mpg.f90 crc.f90 mpg_io.f90 wav_io.f90 filter.f90 psycho.f90 layer1.f90 uzura1win.f90
mpeg audio layer 1 (mp1) とは
mpeg とは、Moving Picture Expert Group の略号で、mpeg-1 は 1992 年に制定された音声付き動画の ISO 国際規格を指します。その動画規格のうちの音声記録部分のみを指したものが、audio layer となります。音声規格には3つの種類があり、それぞれ Layer I, II, III とローマ数字の番号で区別され、番号が増えるほど複雑で高圧縮率になります。Layer I は デジタル・コンパクト・カセットに使われた規格で、CD の 1/4 ほどの 384kbps の圧縮率に適するようになっています。
mp1 は今もよく使われる mp3 に比べてフォーマットなどが極めて単純で素直な形式になっているので、プログラムとして実装しやすくなっています。
参考文献:
D. Pan, "A Tutorial on MPEG/Audio Compression"
https://www.ee.columbia.edu/~dpwe/papers/Pan95-mpeg-ieeemm.pdf
dist10: ISO による参照実装
dist10.tgz で検索してください。
Fortran 2003/08 による実装
Fortran 2003 は Fortran 90/95 にオブジェクト指向 (OO:Object Oriented) の要素を取り入れたもので、それに伴って動的な機能が増えています。Fortran 2008 は、coarray の機能を除けば、Fortran 2003 の不便なところを修正した規格になっています。ここでは主に Fortran 2003 規格にのっとってプログラムを書きましたが、Fortran 2008 で微修正された機能も取り入れてゆくことにしました。
以下では個々のプログラムについて、mpeg 面と Fortran 面の両方について簡単にふれてゆこうかと考えています。
ソース・プログラム
ソースは github に上げましたが、色々おかしいです?メールアドレス設定が変になっていたせい?
https://github.com/cure-honey/uzura1
命名法その他
モジュール名は m_ 、派生型は t_ 、変数名は暗黙型と同じ接頭文字から始めるようにしました。最近の流行ではモジュール名や派生型はそれぞれ _m、_t の接尾文字をつけ、変数名の暗黙型には捕らわれないのが主流のようですが、昔に書いたものなのでこうなっています。
メインプログラムの stop 文、サブルーチンや関数の return 文は、末尾に来る場合に省略しました。private や public の宣言は面倒でもいちいちすることにしました。副プログラムの pure/elemental 等の属性については、気まぐれに付けました。impure は使わないことにしました。OO がらみのクラス変数はスカラーでも動的に割り付けるようにしました。
データ隠匿には無理にこだわらず、セッター/ゲッターを書くのが面倒な時には直接読み書きをすることにしました。陰徳有れば陽報ありといえども、尾生の信じゃあるまいに OO に義理もないので。
配列の多くを規格書に則って 0 から始まるように書き直しましたが、色々失敗だった気がします。理由は(忘れなければw)後述します。
kind.f90
module m_kind
mpeg の規格では有効数字9桁が要求されてるので、倍精度実数を用いる必要があります。
module m_kind
use, intrinsic :: iso_fortran_env
public
integer, parameter :: kd = real64
end module m_kind
Fortran 90 規格制定の時点では、計算機の語長、バイト長、浮動小数点数のフォーマットなどがベンダー依存で統一されていなかったため、数値の型の指定をする kind が何を意味するかは規格では定められませんでした。
最近は 1語=32bit/64bit、1byte=8bit、数値フォーマットは IEEE754 で統一された感があるので、Fortran 2008 では iso_fortran_env というお仕着せのモジュールに変数の型が用意されました。
module m_file_io
mpeg audio encoder は、wav 形式の音声ファイルを入力とし、mpeg 形式の音声ファイルを出力とするので、二つの形式のファイルの I/O が必要となります。
module m_file_io ! abstract interface for file
implicit none
type, abstract :: t_file
contains
procedure(p_open ), deferred :: open_file
procedure(p_close), deferred :: close_file
end type t_file
abstract interface
subroutine p_open(this, fn)
import
class(t_file), intent(in out) :: this
character(len = *), intent(in) :: fn
end subroutine p_open
subroutine p_close(this)
import
class(t_file), intent(in) :: this
end subroutine p_close
end interface
end module m_file_io
ここでは open/close という共通動作を、抽象インターフェースとそれをメソッドとして含む抽象派生型としてまとめてみました。こうすることで、そのインターフェースを持つルーチンの実装が強制されます。
普通のクラスは、はじめに変数を束ねた派生型があって、それを引数とする副プログラムを従属させるという発想だと思いますが、抽象クラスは、はじめに共通動作が束ねてあって、それに変数が従属するという発想なのではないかと思います。そういうわけで、ここでの使い方に適しているように思えたのですが、別に定義した mpg 派生型を合体させることが出来ず、抽象クラスのご利益がよく分からぬままでした。
抽象インターフェースだけ module で受け渡せばいいのかもしれません。
mpg.f90
m_mpg
mpeg/audio ファイルはフレームという独立した単位の集合から成り、1フレームあたり 10ミリ秒程度相当の音声データを持ちます。各フレームはヘッダとボディから成ります。そのヘッダ部分の情報と、それが意味する内容を一括して定義しておきます。
module m_mpg
use m_kind
implicit none
public
type :: t_mpg
integer :: mtype = 3 ! 0:mpeg2.5, 1:---, 2:mpeg2, 3:mpeg1
integer :: layer = 3 ! layer { 1 = III, 2 = II, 3 = I }
integer :: ibit_rate = 12 ! 192 * 2 = 384kbps
integer :: isample_rate ! 0:44.1kHz 1:48kHz 2:32kHz 3:reserved
integer :: ipsychoacoustic = 0
integer :: iemphasis = 0
integer :: ipadding = 0
integer :: icrc = 1 ! CRC16 0 enabled / 1 disabled
integer :: mode = 0 ! 0:stereo 1:joint_stereo 2:dual_channel 3:single_channel
integer :: iprivate_bit = 0 ! unused
integer :: mode_extension = 0 ! 0:4-31 1:8-31 2:12-31 3:16-31 intensity stereo
integer :: icopyright = 0 ! 0: 1:copyright protected
integer :: ioriginal = 1 ! 0:copy 1:original
end type t_mpg
!mpeg1 / audio
integer, parameter :: mpeg_frame_size(3) = [1152, 1152, 384]
integer, parameter :: mpeg_sample_rates(0:3) = [44100, 48000, 32000, 0]
integer, parameter :: mpeg_bit_rates(0:14, 3) = &
reshape( [ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, &
0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, &
0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 414, 448 ], &
[15, 3] )
character (len = *), parameter :: mpeg_mode_names(0:*) = [character(len = 8)::'stereo', 'j-stereo', 'dual-ch', 'mono']
character (len = *), parameter :: mpeg_layer_names(0:*) = [character(len = 3)::'', 'III', 'II', 'I']
character (len = *), parameter :: mpeg_version_names(0:*) = [character(len = 7)::'MPEG2.5', '', 'MPEG-II', 'MPEG-I']
character (len = *), parameter :: mpeg_psy_names(0:*) = [character(len = 6)::'', '', '', '']
character (len = *), parameter :: mpeg_demp_names(0:*) = [character(len = 7)::'none', '50/15us', '', 'CITT']
end module m_mpg
Fortran 2008 では、parameter の数を * 指定で自動で数えてくれます。また 2003 の配列構成子を使うことにより、文字列長を自動でそろえてくれます。parameter は文字列長を * 指定で自動で数えてくれます。
crc.f90
m_crc
mpeg では、CRC16 を埋め込むことによって、情報のうち重要度の高い部分の伝送エラーをチェックすることができます。ただし、エラーの復元のような機能は持ちません。ほとんどのプレーヤーは CRC16 の検出をしません。検出するプレーヤもそのフレームの音を消すだけです。元々はストリーミング通信などの場合の再送信要求などを想定したものと思われます。mpeg では crc の初期値は Z'FFFF' 生成多項式は x^16+x^15+x^2+1 = Z'8005' と定義されています。
module m_crc
use :: m_kind
implicit none
private
public :: crc16
integer, parameter :: igenerator = 32773 !Z'8005' ! 32773 !B'1000000000000101' ! x^16+x^15+x^2+1
contains
subroutine crc16(n, inp, icrc)
integer, intent(in ) :: n, inp
integer, intent(in out) :: icrc
integer :: i, ibit1, ibit2
do i = n - 1, 0, -1
ibit1 = ibits(inp, i, 1)
ibit2 = ibits(icrc, 15, 1)
icrc = iand(int(Z'0000FFFF'), ishft(icrc, 1)) ! trim to 16bit
if ( ieor(ibit1, ibit2) == 1 ) icrc = ieor(icrc, igenerator)
end do
end subroutine crc16
end module m_crc
CRC は九去法のように剰余類を用いて誤りを検出するものです。Numerical Recipes 第二版の "less-numerical algorithms" の章に説明があります。CRC はフレームごとに初期値から累積してゆかなければなりません。つまり履歴に依存する状態を持ちます。ここでは昔のプログラムを bit 演算に書きかえただけにしましたが、ついでに OO により内部状態を持つクラスにした方が良かったかもしれません。
mpg_io.f90
m_mpg_io
mp1 ファイルはフレームのビット列が順に連続的に書き出された集合体になっています。フレームのサイズはビットレートから決まります。
フレームのフォーマットはヘッダ・[CRC16]・ボディとなっており、CRC16 をつけるかどうかは任意です。付けることによりデータ用のボディ領域が 16 ビット分減ります。ボディはビット割り付け情報・スケール係数・割り付けビット列の3部分からなります。
各フレーム末尾には、使われずに余った半端なビットの領域があっても構いません。
module m_mpg_io
use m_kind
use m_file_io
use m_mpg
use m_crc
implicit none
private
public :: t_mpgfile
type, extends(t_file) :: t_mpgfile
private
integer :: iunit
integer :: ipos
character(:), allocatable :: fn
character (len = 20000) :: bit_string
contains
procedure :: open_file => open_mpg_file
procedure :: close_file => close_mpg_file
final :: destroy_file
procedure :: write_bits_1frame
procedure :: clear_bit_buff
procedure :: encode_header
procedure :: encode_crc
procedure :: encode_body
procedure :: put_bits
procedure :: put_bits_c
end type t_mpgfile
contains
subroutine open_mpg_file(this, fn)
class(t_mpgfile), intent(in out) :: this
character(len = *), intent(in) :: fn
integer :: io
open(newunit = this%iunit, file = fn, iostat = io, status = 'unknown', access = 'stream')
if (io /= 0) then
write(*, *) 'i/o error ', io, ' occuerred. file =', this%iunit, 'file name ', fn
stop 'check output file!'
end if
end subroutine open_mpg_file
subroutine close_mpg_file(this)
class(t_mpgfile), intent(in) :: this
close(this%iunit)
end subroutine close_mpg_file
subroutine destroy_file(this)
type(t_mpgfile), intent(in) :: this
call this%close_file()
end subroutine destroy_file
subroutine write_bits_1frame(this, n)
class(t_mpgfile), intent(in) :: this
integer, intent(in) :: n
integer :: i, j, ipos
integer(int8) :: m
ipos = 0
do i = 1, n, 8
m = 0
do j = 7, 0, -1
ipos = ipos + 1
if (ipos > len(this%bit_string)) exit
if (this%bit_string(ipos:ipos) == '1') m = ibset(m, j)
end do
write(this%iunit) m
end do
end subroutine write_bits_1frame
subroutine clear_bit_buff(this, n)
class(t_mpgfile), intent(in out) :: this
integer, intent(in) :: n
this%bit_string = repeat(' ', n)
end subroutine clear_bit_buff
subroutine encode_header(this, mpg)
class(t_mpgfile), intent(in out) :: this
type (t_mpg) , intent(in ) :: mpg
this%ipos = 1 ! reset position to the first bit
call this%put_bits_c('11111111111' ) !sync word
call this%put_bits(2, mpg%mtype ) !mpeg1
call this%put_bits(2, mpg%layer ) !layer 1
call this%put_bits(1, mpg%icrc ) !crc check no
call this%put_bits(4, mpg%ibit_rate ) !bitrate
call this%put_bits(2, mpg%isample_rate ) !sampling frequency 44.1
call this%put_bits(1, mpg%ipadding ) !ipadding
call this%put_bits(1, mpg%iprivate_bit ) !private bit : unused
call this%put_bits(2, mpg%mode ) !stereo
call this%put_bits(2, mpg%mode_extension) !mode
call this%put_bits(1, mpg%icopyright )
call this%put_bits(1, mpg%ioriginal )
call this%put_bits(2, mpg%iemphasis )
end subroutine encode_header
subroutine encode_crc(this, mpg, ialloc_bits)
class(t_mpgfile), intent(in out) :: this
type (t_mpg) , intent(in ) :: mpg
integer , intent(in ) :: ialloc_bits(:, :)
integer :: iband, ichannel, icrc
icrc = int(Z'0000FFFF') ! initialize crc
call crc16(4, mpg%ibit_rate , icrc)
call crc16(2, mpg%isample_rate , icrc)
call crc16(1, mpg%ipadding , icrc)
call crc16(1, mpg%iprivate_bit , icrc)
call crc16(2, mpg%mode , icrc)
call crc16(2, mpg%mode_extension, icrc)
call crc16(1, mpg%icopyright , icrc)
call crc16(1, mpg%ioriginal , icrc)
call crc16(2, mpg%iemphasis , icrc)
do iband = 1, 32
do ichannel = 1, size(ialloc_bits, 2)
call crc16(4, ialloc_bits(iband, ichannel), icrc)
end do
end do
call this%put_bits(16, icrc)
end subroutine encode_crc
subroutine encode_body(this, ialloc_bits, iscale_factor, isubband)
class(t_mpgfile), intent(in out) :: this
integer, intent(in) :: ialloc_bits(:, :), iscale_factor(:, :), isubband(:, :, :)
integer :: iband, ichannel, j, k
do iband = 1, 32
do ichannel = 1, size(ialloc_bits, 2)
call this%put_bits(4, ialloc_bits(iband, ichannel) )
end do
end do
do iband = 1, 32
do ichannel = 1, size(ialloc_bits, 2)
if (ialloc_bits(iband, ichannel) /= 0) call this%put_bits(6, iscale_factor(iband, ichannel) )
end do
end do
do j = 1, 12
do iband = 1, 32
do ichannel = 1, size(ialloc_bits, 2)
k = ialloc_bits(iband, ichannel)
if (k /= 0) call this%put_bits(k + 1, isubband(iband, j, ichannel) )
end do
end do
end do
end subroutine encode_body
subroutine put_bits(this, n, inp)
class(t_mpgfile), intent(in out) :: this
integer, intent(in) :: n, inp
integer :: i, m
associate (ipos => this%ipos, bit_string => this%bit_string)
do i = 1, n
m = 2**(n - i)
if (mod(inp / m, 2) == 1) then
this%bit_string(ipos:ipos) = '1'
else
this%bit_string(ipos:ipos) = '0'
end if
ipos = ipos + 1
end do
end associate
end subroutine put_bits
subroutine put_bits_c(this, str)
class(t_mpgfile), intent(in out) :: this
character(len = *), intent(in) :: str
integer :: i
associate (ipos => this%ipos, bit_string => this%bit_string)
do i = 1, len_trim(str)
if (str(i:i) /= '0' .and. str(i:i) /= '1') stop 'invalid string: subroutine put_bit_c'
this%bit_string(ipos:ipos) = str(i:i)
ipos = ipos + 1
end do
end associate
end subroutine put_bits_c
end module m_mpg_io
mp1 ファイル I/O には、先に定義した抽象クラスを継承したクラスを用いました。
Fortran のオブジェクトにはコンストラクタルーチンを定義できないので、代入以外の初期化処理は別に初期化ルーチンを用意して呼び出す必要があります。ファイナライザ(デストラクタ)はあるので、ファイルのクローズをさせています。
ビット列は0,1からなる文字列として作り上げてゆき、最後にビット列として書き出します。
ファイルをオープンするときのファイル番号は、Fortran 2008 の newunit によって、処理系側にファイル番号を用意させています。これでファイル番号の選択に頭を使う必要がなくなります。バイナリデータは stream モードを使うことで書いたままのデータを垂れ流しで出力できます。
wav_io.f90
入力となる wav ファイルは MS-RIFF (Microsoft Resource Interchange File Format) と呼ばれる構造の一種をしています。これは chunk(塊)から構成されます。chunk は、文字 4 バイトの chunk_id 、4 バイトリトルエンディアン整数の chunk_size、そして chunk_size のバイト数のデータから成ります。
特に wav ファイルは、'RIFF' chunk からなります。 'RIFF' chunk は 'fmt ' chunk, 'data' chunk の2つの chunk から構成されます。(このほかに付加的な情報の chunk が加わることがあります。)
'fmt ' chunk にはチャンネル数、サンプリングレート等の情報が入っています。
'data' chunk の中には PCM 音声データが入っています。CD からリッピングした音声データの場合 サンプリングレート 44.1kHz で 符号付 16ビット・リトルエンディアン整数列がステレオ 2ch (L,R順) で並んでいます。
module m_wav
use m_kind
use m_file_io
implicit none
private
public :: t_wavfile, t_fmt
type :: t_fmt
sequence
character(4):: chunk_id
integer(int32) :: chunk_size
integer(int16) :: format_id, channels
integer(int32) :: sampling_rate
integer(int32) :: bytes_per_sec
integer(int16) :: block_size, bits_per_sample
end type t_fmt
type :: t_data
sequence
character(4) :: chunk_id
integer(int32) :: chunk_size
!! integer(int16), allocatable :: pcm16(:)
end type t_data
type :: t_riffwav
sequence
character(4) :: chunk_id
integer(int32):: chunk_size
character(4) :: formattag
type (t_fmt ) :: fmt
type (t_data) :: dat
end type t_riffwav
type, extends(t_file) :: t_wavfile
private
integer :: iunit
integer :: ipos
character(:), allocatable :: fn
type (t_riffwav) :: riff
integer(int16), allocatable :: pcm16(:) ! <- data of data_chunk
contains
procedure, public :: open_file => open_wav_file
procedure, public :: close_file => close_wav_file
final :: destroy_file
procedure, public :: pcm1frame
procedure, public :: get_channel
procedure, public :: get_sampling_rate
procedure, public :: get_data_size
procedure, public :: get_play_time
end type t_wavfile
contains
subroutine open_wav_file(this, fn)
class(t_wavfile), intent(in out) :: this
character(len = *), intent(in) :: fn
integer :: io
this%fn = fn
open(newunit = this%iunit, file = trim(this%fn), access = 'stream', iostat = io, status = 'old', form = 'unformatted')
if (io /= 0) then
write(*, *) 'i/o error ', io, ' occuerred. file =', this%iunit, 'file name ', fn
stop 'check output file!'
end if
! read wave file little endian assumed
associate (riff => this%riff, fmt => this%riff%fmt, dat => this%riff%dat)
! riff-wave chunk
read(this%iunit) riff
if ( riff%chunk_id /= 'RIFF' ) stop 'this is not RIFF file'
if ( riff%formattag /= 'WAVE' ) stop 'this RIFF file is not in WAVE format'
! fmt chunk
if ( fmt%chunk_id /= 'fmt ' ) stop 'fmt chunk not found'
if ( fmt%format_id /= 1 ) stop 'unknown wave format' ! 1 linear pcm
if ( fmt%bits_per_sample /= 16) stop 'not 16bit data'
select case ( fmt%channels )
case (1)
write(*, '(a, i3, a, i6, a)') 'monoral', fmt%bits_per_sample, 'bit sampling rate', fmt%sampling_rate, 'hz '
case (2)
write(*, '(a, i3, a, i6, a)') 'stereo' , fmt%bits_per_sample, 'bit sampling rate', fmt%sampling_rate, 'hz '
case default
stop 'wave channels must be 1 or 2'
end select
! data chunk
if (dat%chunk_id /= 'data') then
do
inquire(this%iunit, pos = this%ipos)
this%ipos = this%ipos + dat%chunk_size ! skip non-data chunk
read(this%iunit, pos = this%ipos, iostat = io) dat
if (io == -1) stop 'end of file encounterd while searching for a data chunk'
if (dat%chunk_id == 'data') exit
end do
end if
! now file position is at the beginning of pcm data
allocate( this%pcm16( dat%chunk_size / 2 ) )
! read whole pcm data
read(this%iunit) this%pcm16
end associate
return
end subroutine open_wav_file
subroutine close_wav_file(this)
class(t_wavfile), intent(in) :: this
close(this%iunit)
end subroutine close_wav_file
subroutine destroy_file(this) ! finalization routine
type(t_wavfile), intent(in) :: this
call this%close_file()
end subroutine destroy_file
subroutine pcm1frame(this, pcm) ! copy from array
class(t_wavfile), intent(in) :: this
real(kd), intent(in out) :: pcm(:, :)
integer, save :: ipos = 1
pcm = eoshift(pcm, -384, 0.0_kd, 1)
select case (this%riff%fmt%channels)
case (1) !mono
pcm(384:1:-1, 1) = real( this%pcm16(ipos:ipos + 384 - 1), kind = kd ) / 2**15
ipos = ipos + 384
case (2) !stereo
pcm(384:1:-1, 1) = real( this%pcm16(ipos :ipos + 2 * 384 - 1:2), kind = kd ) / 2**15 ! L 16bit int
pcm(384:1:-1, 2) = real( this%pcm16(ipos + 1:ipos + 2 * 384 - 1:2), kind = kd ) / 2**15 ! R
ipos = ipos + 2 * 384
case default
stop 'ichannel must be 1 or 2: subroutine wav_get'
end select
end subroutine pcm1frame
! getters
integer function get_channel(this)
class(t_wavfile), intent(in) :: this
get_channel = this%riff%fmt%channels ! mono:1 stereo:2
end function get_channel
integer function get_sampling_rate(this)
class(t_wavfile), intent(in) :: this
get_sampling_rate = this%riff%fmt%sampling_rate ! in Hz
end function get_sampling_rate
integer function get_data_size(this)
class(t_wavfile), intent(in) :: this
get_data_size = this%riff%dat%chunk_size ! in bytes
end function get_data_size
integer function get_play_time(this) result(isec)
class(t_wavfile), intent(in) :: this
isec = this%riff%dat%chunk_size / this%riff%fmt%bytes_per_sec ! play time in sec
end function get_play_time
end module m_wav
wav ファイルを読み込む派生型は、抽象クラス t_file を継承したものとしました。riff wave 形式のchunk 構造はそのまま派生型として定義通りに並べました。sequence 属性をつけることで定義順にメモリーマップされます。こうすることで派生型の読み込みでファイルから chunk 構造を一気に読み取れます。しかし、sequence 属性を持つ派生型は継承できなくなります。
wav ファイルは一気にすべてのデータを配列内に読み込みます。本来は mpeg 1フレーム分に必要なだけ読み取りを繰り返せば十分です。chunk 構造をそのまま派生型にするためにこのようにしました。メモリーの無駄遣いですが単純化されます。またリトルエンディアンも仮定しました。Intel 系ならば問題ありませんが、IBM や SPARC はビッグエンディアンなので若干問題があります。最近はエンディアンをコンパイル時に選べる場合もありますが、16bit のデータでもちゃんと働くのかよく分かりません。
filter.f90
m_polyphase
mpeg/audio では、まず時系列データである wav ファイルの pcm データをバンドパス・フィルターを使って周波数領域にうつします。バンドパス・フィルターはプロトタイプ・フィルターが一つ数値的に定義されています。そしてこのプロトタイプを周波数領域でずらして(時間領域で三角関数を掛けて)各周波数領域ごとのバンドパス・フィルターを得ます。これを polyphase filter bank と呼びます。
このバンドパス・フィルターを掛けることで、入力された mpeg 1フレーム分の pcm データは周波数方向に 32 個のサブバンドに、時間方向に 12 個に切り分けられたデータとなって出力されます。
module m_polyphase
use m_kind
implicit none
private
public :: t_subband
real(kd), parameter :: pi = 4 * atan(1.0_kd), pi64 = pi / 64
integer :: i, j
! ISO section C.1.3 Analysis subband filter (rounding at 9th decimal)
real(kd), parameter :: coeff(0:31, 0:63) = &
reshape([((1.0e-9_kd * anint(1.0e9_kd * cos(mod( (2 * i + 1) * (j - 16), 128) * pi64)), i = 0, 31), j = 0, 63)], [32, 64])
! ISO Table C.1 -- Coefficients Ci of the Analysis Window
real(kd), parameter :: prototype(0:511) = &
[0.000000000_kd,-0.000000477_kd,-0.000000477_kd,-0.000000477_kd,-0.000000477_kd,-0.000000477_kd,-0.000000477_kd,-0.000000954_kd, &
-0.000000954_kd,-0.000000954_kd,-0.000000954_kd,-0.000001431_kd,-0.000001431_kd,-0.000001907_kd,-0.000001907_kd,-0.000002384_kd, &
-0.000002384_kd,-0.000002861_kd,-0.000003338_kd,-0.000003338_kd,-0.000003815_kd,-0.000004292_kd,-0.000004768_kd,-0.000005245_kd, &
-0.000006199_kd,-0.000006676_kd,-0.000007629_kd,-0.000008106_kd,-0.000009060_kd,-0.000010014_kd,-0.000011444_kd,-0.000012398_kd, &
-0.000013828_kd,-0.000014782_kd,-0.000016689_kd,-0.000018120_kd,-0.000019550_kd,-0.000021458_kd,-0.000023365_kd,-0.000025272_kd, &
-0.000027657_kd,-0.000030041_kd,-0.000032425_kd,-0.000034809_kd,-0.000037670_kd,-0.000040531_kd,-0.000043392_kd,-0.000046253_kd, &
-0.000049591_kd,-0.000052929_kd,-0.000055790_kd,-0.000059605_kd,-0.000062943_kd,-0.000066280_kd,-0.000070095_kd,-0.000073433_kd, &
-0.000076771_kd,-0.000080585_kd,-0.000083923_kd,-0.000087261_kd,-0.000090599_kd,-0.000093460_kd,-0.000096321_kd,-0.000099182_kd, &
0.000101566_kd, 0.000103951_kd, 0.000105858_kd, 0.000107288_kd, 0.000108242_kd, 0.000108719_kd, 0.000108719_kd, 0.000108242_kd, &
0.000106812_kd, 0.000105381_kd, 0.000102520_kd, 0.000099182_kd, 0.000095367_kd, 0.000090122_kd, 0.000084400_kd, 0.000077724_kd, &
0.000069618_kd, 0.000060558_kd, 0.000050545_kd, 0.000039577_kd, 0.000027180_kd, 0.000013828_kd,-0.000000954_kd,-0.000017166_kd, &
-0.000034332_kd,-0.000052929_kd,-0.000072956_kd,-0.000093937_kd,-0.000116348_kd,-0.000140190_kd,-0.000165462_kd,-0.000191212_kd, &
-0.000218868_kd,-0.000247478_kd,-0.000277042_kd,-0.000307560_kd,-0.000339031_kd,-0.000371456_kd,-0.000404358_kd,-0.000438213_kd, &
-0.000472546_kd,-0.000507355_kd,-0.000542164_kd,-0.000576973_kd,-0.000611782_kd,-0.000646591_kd,-0.000680923_kd,-0.000714302_kd, &
-0.000747204_kd,-0.000779152_kd,-0.000809669_kd,-0.000838757_kd,-0.000866413_kd,-0.000891685_kd,-0.000915051_kd,-0.000935555_kd, &
-0.000954151_kd,-0.000968933_kd,-0.000980854_kd,-0.000989437_kd,-0.000994205_kd,-0.000995159_kd,-0.000991821_kd,-0.000983715_kd, &
0.000971317_kd, 0.000953674_kd, 0.000930786_kd, 0.000902653_kd, 0.000868797_kd, 0.000829220_kd, 0.000783920_kd, 0.000731945_kd, &
0.000674248_kd, 0.000610352_kd, 0.000539303_kd, 0.000462532_kd, 0.000378609_kd, 0.000288486_kd, 0.000191689_kd, 0.000088215_kd, &
-0.000021458_kd,-0.000137329_kd,-0.000259876_kd,-0.000388145_kd,-0.000522137_kd,-0.000661850_kd,-0.000806808_kd,-0.000956535_kd, &
-0.001111031_kd,-0.001269817_kd,-0.001432419_kd,-0.001597881_kd,-0.001766682_kd,-0.001937389_kd,-0.002110004_kd,-0.002283096_kd, &
-0.002457142_kd,-0.002630711_kd,-0.002803326_kd,-0.002974033_kd,-0.003141880_kd,-0.003306866_kd,-0.003467083_kd,-0.003622532_kd, &
-0.003771782_kd,-0.003914356_kd,-0.004048824_kd,-0.004174709_kd,-0.004290581_kd,-0.004395962_kd,-0.004489899_kd,-0.004570484_kd, &
-0.004638195_kd,-0.004691124_kd,-0.004728317_kd,-0.004748821_kd,-0.004752159_kd,-0.004737377_kd,-0.004703045_kd,-0.004649162_kd, &
-0.004573822_kd,-0.004477024_kd,-0.004357815_kd,-0.004215240_kd,-0.004049301_kd,-0.003858566_kd,-0.003643036_kd,-0.003401756_kd, &
0.003134727_kd, 0.002841473_kd, 0.002521515_kd, 0.002174854_kd, 0.001800537_kd, 0.001399517_kd, 0.000971317_kd, 0.000515938_kd, &
0.000033379_kd,-0.000475883_kd,-0.001011848_kd,-0.001573563_kd,-0.002161503_kd,-0.002774239_kd,-0.003411293_kd,-0.004072189_kd, &
-0.004756451_kd,-0.005462170_kd,-0.006189346_kd,-0.006937027_kd,-0.007703304_kd,-0.008487225_kd,-0.009287834_kd,-0.010103703_kd, &
-0.010933399_kd,-0.011775017_kd,-0.012627602_kd,-0.013489246_kd,-0.014358521_kd,-0.015233517_kd,-0.016112804_kd,-0.016994476_kd, &
-0.017876148_kd,-0.018756866_kd,-0.019634247_kd,-0.020506859_kd,-0.021372318_kd,-0.022228718_kd,-0.023074150_kd,-0.023907185_kd, &
-0.024725437_kd,-0.025527000_kd,-0.026310921_kd,-0.027073860_kd,-0.027815342_kd,-0.028532982_kd,-0.029224873_kd,-0.029890060_kd, &
-0.030526638_kd,-0.031132698_kd,-0.031706810_kd,-0.032248020_kd,-0.032754898_kd,-0.033225536_kd,-0.033659935_kd,-0.034055710_kd, &
-0.034412861_kd,-0.034730434_kd,-0.035007000_kd,-0.035242081_kd,-0.035435200_kd,-0.035586357_kd,-0.035694122_kd,-0.035758972_kd, &
0.035780907_kd, 0.035758972_kd, 0.035694122_kd, 0.035586357_kd, 0.035435200_kd, 0.035242081_kd, 0.035007000_kd, 0.034730434_kd, &
0.034412861_kd, 0.034055710_kd, 0.033659935_kd, 0.033225536_kd, 0.032754898_kd, 0.032248020_kd, 0.031706810_kd, 0.031132698_kd, &
0.030526638_kd, 0.029890060_kd, 0.029224873_kd, 0.028532982_kd, 0.027815342_kd, 0.027073860_kd, 0.026310921_kd, 0.025527000_kd, &
0.024725437_kd, 0.023907185_kd, 0.023074150_kd, 0.022228718_kd, 0.021372318_kd, 0.020506859_kd, 0.019634247_kd, 0.018756866_kd, &
0.017876148_kd, 0.016994476_kd, 0.016112804_kd, 0.015233517_kd, 0.014358521_kd, 0.013489246_kd, 0.012627602_kd, 0.011775017_kd, &
0.010933399_kd, 0.010103703_kd, 0.009287834_kd, 0.008487225_kd, 0.007703304_kd, 0.006937027_kd, 0.006189346_kd, 0.005462170_kd, &
0.004756451_kd, 0.004072189_kd, 0.003411293_kd, 0.002774239_kd, 0.002161503_kd, 0.001573563_kd, 0.001011848_kd, 0.000475883_kd, &
-0.000033379_kd,-0.000515938_kd,-0.000971317_kd,-0.001399517_kd,-0.001800537_kd,-0.002174854_kd,-0.002521515_kd,-0.002841473_kd, &
0.003134727_kd, 0.003401756_kd, 0.003643036_kd, 0.003858566_kd, 0.004049301_kd, 0.004215240_kd, 0.004357815_kd, 0.004477024_kd, &
0.004573822_kd, 0.004649162_kd, 0.004703045_kd, 0.004737377_kd, 0.004752159_kd, 0.004748821_kd, 0.004728317_kd, 0.004691124_kd, &
0.004638195_kd, 0.004570484_kd, 0.004489899_kd, 0.004395962_kd, 0.004290581_kd, 0.004174709_kd, 0.004048824_kd, 0.003914356_kd, &
0.003771782_kd, 0.003622532_kd, 0.003467083_kd, 0.003306866_kd, 0.003141880_kd, 0.002974033_kd, 0.002803326_kd, 0.002630711_kd, &
0.002457142_kd, 0.002283096_kd, 0.002110004_kd, 0.001937389_kd, 0.001766682_kd, 0.001597881_kd, 0.001432419_kd, 0.001269817_kd, &
0.001111031_kd, 0.000956535_kd, 0.000806808_kd, 0.000661850_kd, 0.000522137_kd, 0.000388145_kd, 0.000259876_kd, 0.000137329_kd, &
0.000021458_kd,-0.000088215_kd,-0.000191689_kd,-0.000288486_kd,-0.000378609_kd,-0.000462532_kd,-0.000539303_kd,-0.000610352_kd, &
-0.000674248_kd,-0.000731945_kd,-0.000783920_kd,-0.000829220_kd,-0.000868797_kd,-0.000902653_kd,-0.000930786_kd,-0.000953674_kd, &
0.000971317_kd, 0.000983715_kd, 0.000991821_kd, 0.000995159_kd, 0.000994205_kd, 0.000989437_kd, 0.000980854_kd, 0.000968933_kd, &
0.000954151_kd, 0.000935555_kd, 0.000915051_kd, 0.000891685_kd, 0.000866413_kd, 0.000838757_kd, 0.000809669_kd, 0.000779152_kd, &
0.000747204_kd, 0.000714302_kd, 0.000680923_kd, 0.000646591_kd, 0.000611782_kd, 0.000576973_kd, 0.000542164_kd, 0.000507355_kd, &
0.000472546_kd, 0.000438213_kd, 0.000404358_kd, 0.000371456_kd, 0.000339031_kd, 0.000307560_kd, 0.000277042_kd, 0.000247478_kd, &
0.000218868_kd, 0.000191212_kd, 0.000165462_kd, 0.000140190_kd, 0.000116348_kd, 0.000093937_kd, 0.000072956_kd, 0.000052929_kd, &
0.000034332_kd, 0.000017166_kd, 0.000000954_kd,-0.000013828_kd,-0.000027180_kd,-0.000039577_kd,-0.000050545_kd,-0.000060558_kd, &
-0.000069618_kd,-0.000077724_kd,-0.000084400_kd,-0.000090122_kd,-0.000095367_kd,-0.000099182_kd,-0.000102520_kd,-0.000105381_kd, &
-0.000106812_kd,-0.000108242_kd,-0.000108719_kd,-0.000108719_kd,-0.000108242_kd,-0.000107288_kd,-0.000105858_kd,-0.000103951_kd, &
0.000101566_kd, 0.000099182_kd, 0.000096321_kd, 0.000093460_kd, 0.000090599_kd, 0.000087261_kd, 0.000083923_kd, 0.000080585_kd, &
0.000076771_kd, 0.000073433_kd, 0.000070095_kd, 0.000066280_kd, 0.000062943_kd, 0.000059605_kd, 0.000055790_kd, 0.000052929_kd, &
0.000049591_kd, 0.000046253_kd, 0.000043392_kd, 0.000040531_kd, 0.000037670_kd, 0.000034809_kd, 0.000032425_kd, 0.000030041_kd, &
0.000027657_kd, 0.000025272_kd, 0.000023365_kd, 0.000021458_kd, 0.000019550_kd, 0.000018120_kd, 0.000016689_kd, 0.000014782_kd, &
0.000013828_kd, 0.000012398_kd, 0.000011444_kd, 0.000010014_kd, 0.000009060_kd, 0.000008106_kd, 0.000007629_kd, 0.000006676_kd, &
0.000006199_kd, 0.000005245_kd, 0.000004768_kd, 0.000004292_kd, 0.000003815_kd, 0.000003338_kd, 0.000003338_kd, 0.000002861_kd, &
0.000002384_kd, 0.000002384_kd, 0.000001907_kd, 0.000001907_kd, 0.000001431_kd, 0.000001431_kd, 0.000000954_kd, 0.000000954_kd, &
0.000000954_kd, 0.000000954_kd, 0.000000477_kd, 0.000000477_kd, 0.000000477_kd, 0.000000477_kd, 0.000000477_kd, 0.000000477_kd ]
type :: t_subband
real(kd), public, allocatable :: subband(:, :, :)
contains
procedure, public :: init
procedure, public :: polyphase_filter12
final :: fin_subband
end type t_subband
contains
subroutine init(this, nchannel)
class(t_subband), intent(in out) :: this
integer , intent(in ) :: nchannel
allocate( this%subband(0:31, 0:11, 0:nchannel - 1) )
end subroutine init
subroutine fin_subband(this)
type(t_subband), intent(in out) :: this
deallocate( this%subband )
end subroutine fin_subband
subroutine polyphase_filter12(this, pcm)
class(t_subband), intent(in out) :: this
real(kd), intent(in) :: pcm(0:, 0:) ! shift to start from 0
integer :: i, ichannel, istart, iend
do i = 0, 11
iend = 863 - 32 * i
istart = iend - 512 + 1
do ichannel = 0, ubound(pcm, 2)
call polyphase_filter( prototype * pcm(istart:iend, ichannel), this%subband(:, i, ichannel) )
end do
end do
contains
subroutine polyphase_filter(z, s)
real(kd), intent(in ) :: z(0:)
real(kd), intent(in out) :: s(:)
real(kd) :: y(0:63)
integer :: j
forall(j = 0:63) y(j) = sum(z(j:511:64))
s = matmul(coeff, y)
end subroutine polyphase_filter
end subroutine polyphase_filter12
end module m_polyphase
Fortran 90 以降では、1行の許される長さは 132 文字までなので、醜くなりますが表を詰めました。プロトタイプ・フィルターの定義より pcm データは配列の添え字の小さい方に時間的に後ろのデータを来るように並べなければなりませんでした。
psycho.f90
心理音響解析は、客観的な音波がどのように主観的な音像ととして認知されるかを解析するものです。
耳の周波数分解能と強度分解能以下であれば元の音波から変化しても、主観的な音像は変わらないことを利用して音声データの圧縮を図るために必要です。
mp1 に関しては、あるフレームに属する各サブバンドごとに許される誤差を求めます。先に述べたように各フレームは 12 個の時間領域に分割されているので、32 個のサブバンド毎にそれぞれの時間領域で許容誤差を求め、12 個中での最小値がそのフレームでの許容誤差となります。
この許容誤差はマスキングしきい値と同一視されます。マスキングしきい値はある音に他の周波数と強度を持つ音を加えた時に認知されるかどうかを表すの閾値の関数です。とくに静音時に対して求めた閾値は絶対閾値と呼ばれます。
心理音響解析は非常に複雑で難解なものですが、ネットなどで拾い集めた情報を総合すると、周波数方向には bark 単位、強度方向には $I^\alpha$$(\alpha\simeq0.3)$ の座標系に変換すると線形重ね合わせが成立するようです。
bark 単位系は人間の周波数分解能の非線形性を、おおむね線形に換算するもので、ここでは周波数方向の座標変換を統一するため、はじめに周波数分解能関数を定義しその逆数の積分で pseud-bark 単位系を定義しました。
また絶対閾値は音に対する認知の感受性を表すもので、一種のばね定数のようなものと考えられるので、マスキング閾値を求める時には、音の強度は絶対閾値で規格化するのが有効なようです。マスキングは bark 単位系で規格化された左右非対称な三角形で近似されます(spreading function)。
正しく理解しているか分かりませんが mp1 に対してはこの程度の解析で最低限成り立つのではないかと思います。
m_fft
2のべき乗の高速フーリエ変換を行なうルーチンです。ハニング窓も定義して用いています。
polyphase filter bank の出力は 32 個のバンド間に重なりがあるように定義されているので、
pcm データを FFT で周波数領域に変換します。polyphase filter bank で変換される pcm データ数は、2のべき乗ではないのですが大きな問題にはなりません。
module fft_module
use m_kind
implicit none
private
public :: fft_window
integer , parameter :: np2 = 9, nn = 2**np2, nn_2 = nn / 2 ! 2^9 = 512
real(kd), parameter :: pi = 4 * atan(1.0_kd), pi2 = 2 * pi
real(kd), parameter :: pi2_n = pi2 / nn
integer, save :: indx(nn)
integer, private :: i_
complex(kd), parameter :: omega(0:*) = [(exp(cmplx(0.0_kd, pi2_n * i_, kind = kd)), i_ = 0, nn - 1)]
real(kd) , parameter :: hann_window(*) = [(0.5_kd * (1.0_kd - cos(pi2_n * i_)) , i_ = 0, nn - 1)]
contains
subroutine fft_initialize()
integer :: i, j2, k, n
do i = 1, nn
n = 0
k = i - 1
do j2 = np2 - 1, 0, -1
n = n + mod(k, 2) * 2**j2
k = k / 2
end do
indx(i) = n + 1 ! indx = 1..n
end do
end subroutine fft_initialize
subroutine fft_window(x, y)
real (kd), intent(in ) :: x(:)
complex (kd), intent(out) :: y(:)
logical :: qfirst = .true.
if (qfirst) then
qfirst = .false.
call fft_initialize()
end if
y = hann_window * x / nn
call fft2(y)
end subroutine fft_window
subroutine fft2(fft) ! fft_2^np2
complex (kd), intent(in out) :: fft(:)
complex (kd) :: tmp1, tmp2, c1
integer :: i, j, k2, m1, m2, iphase, kn2
fft = fft(indx)
do k2 = 1, np2
kn2 = 2**(k2 - 1)
do i = 1, nn, 2* kn2
do j = 1, kn2
iphase = 2**(np2 - k2)*(j - 1)
c1 = omega( mod(iphase, nn) )
m1 = i + j - 1
m2 = m1 + kn2
tmp1 = fft(m1)
tmp2 = c1 * fft(m2)
fft(m1) = tmp1 + tmp2 ! 1 = x^2 ; x = 1 or -1
fft(m2) = tmp1 - tmp2
end do
end do
end do
end subroutine fft2
end module fft_module
m_psycho
最終的な出力は、音声信号強度とマスクの比の対数 SMR となります。これが許容される相対的誤差を表します。
・以下で用いた定式化
周波数分解能バンド幅(これを周波数方向の基本的な基準とします。)
\mathrm{bw}(f)=25+75(1+1.4(f/1000)^2)^{0.69}
$f$ は Hz 単位。
上で定義したバンド幅とつじつまが合うように定義した pseud-bark
b(f_0)=\int_0^{f_0}{df\over \mathrm{bw}(f)}
ある周波数 $f_0$ での主観的な音強度関数 $I(f_0)$ はバーク座標系で、αノルムを取る。
I(b_0)=\left(\int_{b_0-0.5}^{b_0+0.5}|\mathrm{FFT}(b)|^{2\alpha}db\right)^{1\over\alpha}
周波数で書き直すと、
I(f_0)=\left(\int_{f_0-\mathrm{bw(f_0)/2}}^{f_0+\mathrm{bw(f_0)/2}}|\mathrm{FFT}(f)|^{2\alpha}{df\over\mathrm{bw(f)}}\right)^{1\over\alpha}
ここで $\mathrm{FFT(f)}$ は周波数 $f$ でのフーリエ成分、$\mathrm{bw(f)}$ は周波数 $f$ での周波数分解能バンド幅、$\alpha$ は音の認知に関わる定数をそれぞれ表します。
ある周波数 $f_0$ での音マスキング関数 $\mathrm{Mask}(b_0)$ はバーク座標系で、
\mathrm{Mask}(b_0)=\left\{\int^{\infty}_{0}\left( \mathrm{sp}(b_0-b){|\mathrm{FFT}(b)|^2\over\mathrm{ath}(b)}\right)^{\alpha}{db} \right\}^{1\over\alpha}
周波数で書き直すと、
\mathrm{Mask}(f_0)=\left\{\int^{\infty}_{0}\left( \mathrm{sp}(b(f_0)-b(f)){|\mathrm{FFT}(f)|^2\over\mathrm{ath}(f)}\right)^{\alpha}{df\over\mathrm{bw(f)}} \right\}^{1\over\alpha}
ここで $\mathrm{sp}(b)$ は spreading function、$\mathrm{b}(f)$ はバーク関数、$\mathrm{ath}(f)$ は絶対マスキング閾値関数をそれぞれ表します。
これらを離散化すると、それぞれ以下のようになります。
I_j=\left(\sum_i|\mathrm{FFT}_i|^{2\alpha}{1\over\mathrm{bw}_i}\cdot\mathrm{scale}\right)^{1\over\alpha}
\mathrm{Mask}_j=\left\{\sum_i\left(\mathrm{sp}_{j,i}{|\mathrm{FFT}_i|^2\over\mathrm{ath}_i}\right)^{\alpha}{1\over\mathrm{bw_i}}\cdot\mathrm{scale}\right\}^{1\over\alpha}
ここで $\mathrm{scale}$ は、サンプリング周波数/2*フーリエ変換の分割数。測度に相当。
(昔のノートを丸写ししているのでおかしかったらゴメンw 前に書いた式はおかしかった。連続を離散にするところで無理に規格化したかったようだ。FFTの本数が荒いと式の上では1になる所がならないので、いじくって意味不明に。)
module m_psycho
use m_kind
implicit none
private
public :: psychoacoustics
real(kd), save :: freq_fft(0:256), pseud_bark(0:256), ath_fft(0:256)
real(kd), save :: crbw_fft(0:256), cbwl_fft(0:256)
real(kd), save :: sp(256, 256) ! spreading function for masking
real(kd), save :: scale
real(kd), parameter :: alpha = 0.27d0 ! non-linear factor
contains
subroutine psychoacoustics(pcm, isample_rate, smr)
use fft_module
real (kd), intent(in ) :: pcm(:, :)
integer , intent(in ) :: isample_rate
real (kd), intent(out) :: smr(:, :)
complex(kd) :: cfft(512)
integer :: ichannel, i0, i1
logical, save :: qfirst = .true.
if (qfirst) then
qfirst = .false.
call init_absolute_threshold(isample_rate)
end if
i0 = 161
i1 = i0 + 512 - 1
do ichannel = 1, size(pcm, 2)
call fft_window(pcm(i0:i1, ichannel), cfft)
call calc_smr(cfft, smr(:, ichannel))
end do
end subroutine
subroutine init_absolute_threshold(isample_rate)
integer, intent(in) :: isample_rate
real (kd) :: freq, tmp
integer :: i, m, i0, i1
do i = 0, size(freq_fft) - 1
freq = real(isample_rate, kind = kd) / 2.0_kd / 1000.0_kd &
* real(i, kind = kd) / real(size(freq_fft), kind = kd)
ath_fft(i) = 3.64_kd * freq**(-0.8_kd) & !
- 6.5_kd * exp(-0.6_kd * (freq - 3.3_kd)**2) + 0.001_kd * freq**4.0_kd
freq = freq * 1000.0_kd ! khz -> hz
freq_fft(i) = freq
end do
scale = real(isample_rate, kind = kd) / 2 / 256 ! freq to fft-line scale
crbw_fft = critical_band_width( freq_fft ) ! critical band width in hz
cbwl_fft = decibel( crbw_fft ) ! critical band width in log
tmp = 0.0_kd ! pseud bark: integrate critical band
do m = 1, 256
tmp = tmp + 1.0_kd / crbw_fft(m) ! integration
pseud_bark(m) = tmp * scale
end do
! spreading function
forall(i = 1:256, m = 1:256) sp(i, m) = spreading( pseud_bark( i ) - pseud_bark( m ) ) ! top normalized to 1.0
end subroutine init_absolute_threshold
pure elemental real(kd) function spreading(z)
real(kd), intent(in) :: z ! pseud-bark
if ( z > 0.0d0 ) then
spreading = -25.0_kd * z
else
spreading = 75.0_kd * z
end if
spreading = max(-160.0_kd, spreading)
end function spreading
pure elemental real(kd) function critical_band_width(f)
real(kd), intent(in) :: f ! hz
real(kd), parameter :: gamma = 0.69_kd
critical_band_width = 25.0_kd + 75.0_kd * ( 1.0_kd + 1.4_kd * (f / 1000.0_kd)**2 )**gamma
! critical_band_width = 100.0d0 * ( 1.0d0 + 1.0d0 * (f / 1000.0d0)**2 )**gamma
end function critical_band_width
pure elemental real(kd) function decibel(x)
real (kd), intent(in) :: x
real (kd) :: tmp
tmp = max(1.0e-100_kd, x)
decibel = 10.0e0_kd * log10(tmp)
end function decibel
subroutine calc_smr(cfft, smr)
complex(kd), intent(in) :: cfft(0:)
real (kd), intent(out) :: smr(:)
real (kd), parameter :: pi = 4 * atan(1.0_kd), pi2 = 2 * pi ! fortran2003
real (kd) :: snr(32), rmnr(32)
real (kd) :: xa(0:256), ya(0:256), za(0:256)
integer :: iband, i, m, i0, i1
xa = 2 * decibel( abs(cfft(0:256)) )
ya = 0.0_kd
do i = 1, 256 ! convolution of spreading function
do m = 1, 256 ! i maskee, m masker
ya(i) = ya(i) + 10.0_kd**( ((sp(i, m) + xa(m) - ath_fft(m)) * alpha - cbwl_fft(m)) / 10.0_kd ) ! non-linear sum
end do
end do
ya = max(decibel(ya * scale) / alpha - 11.5_kd, ath_fft - 90.3_kd) ! 11.5 mask factor, 90.3dB = 2^15 ATH shift empirical
! effective spl
do i = 1, 256
m = nint( crbw_fft(i) / scale )
i0 = max(i - m / 2, 1) !f0 - bw(f0) / 2
i1 = min(i0 + m - 1, 256) !f0 + bw(f0) / 2
za(i) = sum( 10.0_kd**( (xa(i0:i1) * alpha - cbwl_fft(i)) / 10.0_kd ) )
end do
za = decibel(za * scale) / alpha
! smr = snr' - mnr'
do iband = 1, 32
m = (iband - 1) * 8 + 1
i0 = m - nint( crbw_fft(m) / 2 / scale ) ! fl - bw(fl) / 2 ; subband [fl..fh]
i0 = max(i0, 1)
m = m + 7
i1 = m + nint( crbw_fft(m) / 2 / scale ) ! fh + bw(fh) / 2
i1 = min(i1, 256)
snr(iband) = maxval( za(i0:i1) )
rmnr(iband) = minval( ya(i0:i1) )
end do
smr = snr - rmnr
end subroutine calc_smr
end module m_psycho
この心理音響解析が一番時間のかかる部分です。特に spreading function を畳み込む部分に時間がかかっています。絶対マスキング関数(静音閾値)やバンド幅等は文献値をできるだけそのまま用いることにしました。
layer1.f90
バンドパスフィルターを通した信号情報と心理音響解析で得られた相対許容誤差から、各サブバンドごとにビットを割り振って、許容誤差内に量子化誤差が収まるように量子化してゆきます。相対的量子化誤差は SNR 比として与えられます。
m_layer1
module m_layer1
use m_kind
implicit none
private
public :: isubband_normalization, bit_allocation, iquantization
integer, private :: i_
! ISO Table B.1 -- Layer I,II scalefactors
real(kd), parameter :: scale_factor(0:63) = &
[(1.0e-14_kd * anint(1.0e14_kd * 2.0_kd * 2.0_kd**(-real(i_, kd) / 3.0_kd )), i_ = 0, 63)]
! ISO Table C.2 -- Layer I Signal-to-Noise Ratios
!real(kd), parameter :: snr(0:*) =[ 0.00_kd, 7.00_kd, 16.00_kd, 25.28_kd, 31.59_kd, 37.75_kd, 43.84_kd, &
! 49.89_kd, 55.93_kd, 61.96_kd, 67.98_kd, 74.01_kd, 80.03_kd, 86.05_kd, 92.01_kd]
! empirically better SNR = 20 * log(2^n)
real(kd), parameter :: snr(0:14) = [(20.0_kd * LOG10(2.0_kd) * i_, i_ = 0, 14)]
! ISO Table C.3 -- Layer I Quantization Coefficients
real(kd), parameter :: tmp(14) = [( 1.0_kd / 2.0_kd**real(i_ + 1, kd), i_ = 1, 14)]
real(kd), parameter :: a(14) = [ 1.0e-9_kd * anint(1.0e9_kd * (1.0e0_kd - tmp))]
real(kd), parameter :: b(14) = [-1.0e-9_kd * anint(1.0e9_kd * tmp)]
contains
pure function isubband_normalization(subband) result(iscale_factor)
real(kd), intent(in) :: subband(0:, 0:, 0:)
integer :: iscale_factor(0:31, 0:size(subband, 3))
integer :: ichannel, iband
do ichannel = 0, size(subband, 3)
do iband = 0, 31
iscale_factor(iband, ichannel) = iscale_fac_sub(subband(iband, :, ichannel))
end do
end do
contains
pure integer function iscale_fac_sub(x) result(ires)
real(kd), intent(in) :: x(0:)
do ires = 62, 0, -1
if (maxval(abs(x), dim = 1) < scale_factor(ires)) exit
end do
end function iscale_fac_sub
end function isubband_normalization
subroutine bit_allocation(smr, max_bits, itot_bits, ialloc_bits)
real(kd), intent(in) :: smr(:, :)
integer , intent(in) :: max_bits
integer , intent( out) :: ialloc_bits(:, :)
integer , intent(in out) :: itot_bits
integer :: max_pos(2), k
real(kd) :: rmnr(32, 2)
ialloc_bits = 0
rmnr = smr - snr(0)
do
select case (max_bits - itot_bits)
case (0:11) ! no more bits left
exit
case (12:29) ! cannot allocate to new place
max_pos = maxloc(rmnr, mask = ialloc_bits < 13 .AND. ialloc_bits /= 0)
case (30:) !
max_pos = maxloc(rmnr, mask = ialloc_bits < 13)
if (rmnr( max_pos(1), max_pos(2) ) < -0.44d0) then ! threshold margin 10% -0.46 20% -1.0
max_pos = maxloc(rmnr, mask = ialloc_bits == 0)
if (max_pos(1) == 0) max_pos = maxloc(rmnr, mask = ialloc_bits < 13)
end if
case default
stop 'impossible!: bit_allocation'
end select
if (max_pos(1) == 0) exit
k = ialloc_bits( max_pos(1), max_pos(2) )
if (k == 0) then
itot_bits = itot_bits + 6 + 12 * 2 ! first time: scalefactor 6 bits + 12 * 2 bits
else
itot_bits = itot_bits + 12 ! 12 * 1 bit
end if
ialloc_bits( max_pos(1), max_pos(2) ) = k + 1
rmnr ( max_pos(1), max_pos(2) ) = smr( max_pos(1), max_pos(2) ) - snr(k + 1)
end do
end subroutine bit_allocation
pure function iquantization(ialloc_bits, subband, iscale_factor) result(isubband)
integer , intent(in) :: ialloc_bits(0:, 0:), iscale_factor(0:, 0:)
real(kd), intent(in) :: subband(0:, 0:, 0:)
integer :: isubband(0:31, 0:11, 0:ubound(subband, 3))
integer :: ichannel, iband, k, m
real(kd) :: s(12)
do ichannel = 0, ubound(subband, 3)
do iband = 0, 31
k = ialloc_bits(iband, ichannel)
m = iscale_factor(iband, ichannel)
if (k /= 0) THEN
s = a(k) * subband(iband, :, ichannel) / scale_factor(m) + b(k)
isubband(iband, :, ichannel) = floor( (s + 1.0_kd) * 2**k )
else
isubband(iband, :, ichannel) = 0
end if
end do
end do
end function iquantization
end module m_layer1
ここで bit 分配位置を決めるのに maxloc 関数を用いましたが、この関数は入力配列の何番目かという値を返すので、1から始まる配列でない場合は先頭のオフセット値をずらしてやる必要があります。これが面倒なので、ここでは配列を0から始まるように取りませんでした。
uzura1win.f90
メインプログラムでは、ファイルサイズなどから総フレーム数などを求め、その回数分だけ反復処理を行います。mpeg/audio はバンドパスフィルターで処理・復号することで時間的に遅延が生じるので、初期データ位置の調整などをすべきなのですが、簡単のため省略しました。
名目上のビットレートを合わせるために、時々フレーム長を付け足さねばならないことがります。これは padding と呼ばれる処理です。CD の 44.1KHz のような半端なサンプリングレートの wav ファイルを処理するときに必要となります。やらなくても問題はありません。
main program
program uzura1
use m_kind
use m_mpg
use m_wav
use m_mpg_io
use m_polyphase
use m_psycho
use m_layer1
implicit none
integer , allocatable :: iscale_factor(:, :), isubband(:, :, :), ialloc_bits(:, :)
real(kd), allocatable :: pcm(:, :), smr(:, :)
integer :: max_bits, itot_bits
integer :: nchannel, iframe = 1, ntotal_frames
character(len = :), allocatable :: file_name, fn_in, fn_out
type(t_mpg) :: mpg
type(t_subband), allocatable :: subb
type(t_wavfile), allocatable :: wav
type(t_mpgfile), allocatable :: mp1
call get_option(mpg, file_name)
fn_in = trim(file_name) // '.wav'
fn_out = trim(file_name) // '.mp1'
allocate(wav, mp1)
call wav%open_file(fn_in ) ! read whole wav file
call mp1%open_file(fn_out)
call pr_info(mpg)
call pr_play_time( wav%get_play_time() )
nchannel = wav%get_channel()
allocate( pcm(864, nchannel), smr(32, nchannel) )
allocate( iscale_factor(32, nchannel), isubband(32, 12, nchannel), ialloc_bits(32, nchannel) )
allocate(subb)
call subb%init(nchannel)
ntotal_frames = wav%get_data_size() / (mpeg_frame_size(mpg%layer) * nchannel * 2)
pcm = 0.0d0
do while(iframe <= ntotal_frames )
call get_maxbits(mpg, max_bits)
call mp1%clear_bit_buff(max_bits)
call mp1%encode_header(mpg)
itot_bits = 32
call wav%pcm1frame(pcm)
call subb%polyphase_filter12(pcm(:, :))
smr = 0
call psychoacoustics(pcm, wav%get_sampling_rate(), smr)
iscale_factor = isubband_normalization(subb%subband)
itot_bits = itot_bits + 4 * 32 * nchannel ! 4*32*nch bits required for the scale factor bits :
if (mpg%icrc == 0) itot_bits = itot_bits + 16 ! 16bits required for the crc
call bit_allocation(smr, max_bits, itot_bits, ialloc_bits)
IF (mpg%icrc == 0) call mp1%encode_crc(mpg, ialloc_bits)
isubband = iquantization(ialloc_bits, subb%subband, iscale_factor)
CALL mp1%encode_body(ialloc_bits, iscale_factor, isubband)
CALL mp1%write_bits_1frame(max_bits)
iframe = iframe + 1
if (mod(iframe, 200) == 0) call update_status(iframe, ntotal_frames)
end do
write(*, *) 'total frames', iframe - 1, '/', ntotal_frames
deallocate(subb)
deallocate(wav) ! close file & deallocte
deallocate(mp1) ! close file & deallocte
contains
subroutine pr_info(mpg)
type (t_mpg), intent(in) :: mpg
write(*, *) 'uzura1 (mpeg-1 audio/layer-I encoder) ver.0.4 '
write(*, *) 'psychoacoustic model ', mpeg_psy_names(mpg%ipsychoacoustic), &
' bit rate (kbps)', mpeg_bit_rates(mpg%ibit_rate, mpg%layer)
if (mpg%icrc == 0) write(*, *) 'crc16 error protection enabled'
end subroutine pr_info
subroutine pr_play_time(itot_sec)
integer, intent(in) :: itot_sec
integer :: ihour, imin, isec
ihour = itot_sec / 3600
imin = mod(itot_sec, 3600) / 60
isec = mod( mod(itot_sec, 3600) , 60 )
write(*, '(a, i3, a, i2, a, i2)') ' playtime ', ihour, ':', imin, ':', isec
end subroutine pr_play_time
subroutine update_status(iframe, itot_frames)
integer, intent(in) :: iframe, itot_frames
integer :: it(8), ielapsed, iel_min, iel_sec
integer, save :: istart
logical, save :: qfirst = .true.
real :: percent
character (len = 10) :: time, date, zone
call date_and_time(date, time, zone, it)
if (qfirst) then
istart = it(5) * 3600 + it(6) * 60 + it(7)
qfirst = .false.
end if
ielapsed = it(5) * 3600 + it(6) * 60 + it(7) - istart
iel_min = ielapsed / 60
iel_sec = mod(ielapsed , 60)
percent = real(100 * iframe) / real(itot_frames)
write(*, '(a, f6.2, a, i4, 2(a, i2), 3(a, i2.2), a, i4.2, a, i2.2, a)') &
'+processed...', percent, '% ', &
it(1), '/', it(2), '/', it(3), ' ', it(5), ':', it(6), ':', it(7), &
' time elapsed ', iel_min, 'min ', iel_sec, 'sec'
end subroutine update_status
subroutine get_maxbits(mpg, max_bits)
type(t_mpg), intent(in out) :: mpg
integer , intent(out) :: max_bits
integer , save :: islot_size
real (kind = 8), save :: padding, fslot_size
logical , save :: qfirst = .true.
if (qfirst) then
qfirst = .false.
padding = 0.0d0
call calc_slot_size(mpg, islot_size, fslot_size)
end if
padding = padding + fslot_size
if (padding > 1) then
mpg%ipadding = 1
padding = padding - 1.0d0
else
mpg%ipadding = 0
end if
max_bits = ( islot_size + mpg%ipadding ) * 32
end subroutine get_maxbits
subroutine calc_slot_size(mpg, islot_size, fslot_size)
type(t_mpg), intent(in) :: mpg
integer , intent(out) :: islot_size
real(kd), intent(out) :: fslot_size
real(kd) :: aslot_size
aslot_size = 12.0d0 * 1000.0d0 * real(mpeg_bit_rates(mpg%ibit_rate, mpg%layer), kind = kd) &
/ real(mpeg_sample_rates(mpg%isample_rate), kind = kd)
aslot_size = 1.0d-3 * anint(1.0d3 * aslot_size)
islot_size = int(aslot_size)
fslot_size = aslot_size - islot_size
end subroutine calc_slot_size
subroutine get_option(mpg, fn_in)
type(t_mpg), intent(in out) :: mpg
character (len = :), allocatable, intent(out) :: fn_in
character (len = 80) :: buffer
character (len = 6) :: fmt
integer :: narg, iarg, length
iarg = 0
narg = command_argument_count() + 1
do
iarg = iarg + 1
if (iarg >= narg) call print_option()
call get_command_argument(iarg, buffer)
if (buffer(1:1) /= '-') exit
select case(trim(buffer))
case ('-b')
iarg = iarg + 1
if ( iarg >= narg ) call print_option()
call get_command_argument(iarg, buffer, length)
write(fmt, '(a, i1, a)') '(i', length, ')'
read(buffer, fmt) mpg%ibit_rate
if (mpg%ibit_rate < 1 .or. mpg%ibit_rate > 14) call print_option()
case ('-crc')
mpg%icrc = 0 ! crc16 on
case ('-c')
mpg%icopyright = 1 ! copyrigt on
case ('-o')
mpg%ioriginal = 1 ! original on
case default
call print_option()
end select
end do
fn_in = trim(buffer)
end subroutine get_option
subroutine print_option()
write(*, *) 'Usage : uzura -option file_name '
write(*, *) ' : file_name.wav -> file_name.mp1'
write(*, *) 'Option: -b 1..14 bitrate '
write(*, *) ' -crc CRC16 error protection on'
write(*, *) ' -c copyright flag on'
write(*, *) ' -o original flag on'
stop
end subroutine print_option
end program uzura1
メインプログラムでは、入力ファイル名をコマンドライン引数としてうけとり、同じ名前の mp1 拡張子を持つファイルを出力します。
mp1 エンコーダの最小の実装としては、CRC は省略しても問題ありません。また心理音響解析も SMR に0か何か適当な値を入れておけば省略しても問題ありません。(単にビットの割り振りが非効率になるだけです。)