1/3オクターブ周波数のリストを得る

目次

標準数とは

1/3オクターヴ幅での周波数リストが欲しい時があります。20, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315 Hz... というやつです。実はこのリスト、つまり1/3オクターヴごとの周波数の値の決め方についてはJIS Z8601「標準数」やISO 3 "Preferred Numbers"に規定があります。

ja.wikipedia.org

その前に、オクターヴと周波数の関係についておさらいしましょう。物理的な1オクターヴは周波数2倍と同等ですが、1/3オクターヴの周波数が欲しいとき、たとえば基準周波数を1000 Hzとしたら、それに21/3、22/3、23/3を掛け算すると、それぞれ1259.9、1587.4、2000 Hzとなります。1000 Hzの1/3オクターヴ上が1259.9 Hz、さらにその1/3オクターヴ上が1587.4 Hzという具合です。さて、オクターヴと周波数は指数の関係にありますので、1000、1259.9、1587.4、2000という値は対数軸上で等間隔になっています。

オクターヴに似たものにディケードがあります。1ディケードは周波数10倍で、上記と同様にして101/10、102/10、103/10を掛け算すると、それぞれ1258.9、1584.9、1995.3 Hzになります。

オクターヴとディケードのどちらを採用するかは、分野によるのではないかと思いますが、ディケードを採用すると前述のように1000 Hzを基準に周波数がぴったり2倍の2000 Hzは出てきませんし、オクターヴを採用すると1000 × 210/3 = 10079.4と、周波数10倍が10000ちょうどになりません。

計算上でぴったりとした周波数が必要であれば上記のように計算すればよいのですが、見た目がきれいな値でも良い場合があります。そのときに使えるのが標準数です。扱いやすい数列を用意しておいて、その2倍や10倍などを計算することで最初に挙げたようなキリの良い(良さそうに見える)数列を得るものです。JIS規格で「扱いやすい数列」にはR5, R10, R20, R40, R80などが用意されています。Rのあとの数は1ディケード(10倍)を対数軸上で何分割したかをあらわしていて、たとえばR5なら1.00, 1.60, 2.50, 3.00, 6.30の5つの数が使われます。3つ隣の値がオクターヴ関係になるという性質がありがたいからか、聴覚の臨界帯域と1/3オクターヴの相性がよいからか、音の世界ではR10(1.00, 1.25, 1.60, 2.00, 2.50, 3.15, 4.00, 5.00, 6.30, 8.00)が使われることが多いです。

Common Lispでの実装

この標準数を任意の周波数範囲で欲しいときに使えるプログラムをCommon Lispで作ってみました。let*のなかで計算に必要な数たちを計算しています。R10は前述の通りで、N1とN2は欲しい数がおよそ10の何乗のオーダーなのかを計算します。たとえば20~20000 Hzの間の数が欲しい時には、その範囲は101~105に含まれますので、N1は1、N2は5にします。ffは何をしているかというと、R10の内容を(N1とN2をもとに計算した)101, 102, 103, 104, 105それぞれと掛け合わせ、それをリストにしています。全ての組み合わせを掛け算するために2重ループを使い、1つのリストにするためにreduce #'appendを使っています。最後のループでは、指定した範囲の値だけをcollectで集めています。

;;; preferred frequencies
;;;
;;; (pref-freqs)
;;; => (20.0 25.0 31.5 40.0 50.0 63.0 80.0 100.0 125.0 160.0 200.0
;;;     250.0 315.0 400.0 500.0 630.0 800.0 1000.0 1250.0 1600.0
;;;     2000.0 2500.0 3150.0 4000.0 5000.0 6300.0 8000.0 10000.0
;;;     12500.0 16000.0 20000.0)
;;;
;;; (pref-freqs :min 10 :max 100)
;;; => (10.0 12.5 16.0 20.0 25.0 31.5 40.0 50.0 63.0 80.0 100.0)
;;; 
(defun pref-freqs (&key (min 20) (max 20000))
  "Calculate R10 series of preferred frequencies (JIS Z8601)."
  (let* ((R10 '(1.00 1.25 1.60 2.00 2.50 3.15 4.00 5.00 6.30 8.00))
         (N1 (floor   (log min 10)))
         (N2 (ceiling (log max 10)))
         (ff
           (reduce #'append
                   (loop for sc from N1 to N2
                         collect
                         (loop for fr in R10
                               collect (* (expt 10 sc) fr))))))
    (loop for f in ff
          if (and (<= min f) (<= f max))
            collect f)))

実行例

コメントにも使い方が書いてありますが、SBCLでの実行結果は以下のようになりました。

CL-USER>  (pref-freqs)   ; デフォルトでは20~20000 Hzの範囲
(20.0 25.0 31.5 40.0 50.0 63.0 80.0 100.0 125.0 160.0 200.0 250.0 315.0 400.0
 500.0 630.0 800.0 1000.0 1250.0 1600.0 2000.0 2500.0 3150.0 4000.0 5000.0
 6300.0 8000.0 10000.0 12500.0 16000.0 20000.0)

CL-USER> (pref-freqs :min 10 :max 100)   ; minとmax片方だけの指定も可能です
(10.0 12.5 16.0 20.0 25.0 31.5 40.0 50.0 63.0 80.0 100.0)

似たようなことをJuliaでやってみると……

だいぶ前に似たようなことをJuliaでも作っていました。上記の2重ループを使った方法とはアプローチが違うので紹介します。(Julia対Lispという比較をしたいわけではなく、あくまでも「アプローチの違いを味わいたい」という目的です。)

最低限の標準数の計算をするだけのプログラムは次のようなものになります。1行目で10のべき乗の列ベクトルと、R10の行ベクトルの積をとって、すべての組み合わせの積が入った行列を作ります。それを(値の並べ替えのために)転置してから[:]で1列にするというものです。使い勝手を良くするための周波数範囲指定などは入っていませんが、言語仕様に行列計算が組み込まれているのでここまで簡単にできます。おそらくCommon Lispでも行列計算ライブラリを使えば同じことができるでしょう。

julia> pref_freqs = 10.0 .^ range(1, 4) * [1.00 1.25 1.60 2.00 2.50 3.15 4.00 5.00 6.30 8.00]
4×10 Matrix{Float64}:
    10.0     12.5     16.0     20.0     25.0     31.5     40.0     50.0     63.0     80.0
   100.0    125.0    160.0    200.0    250.0    315.0    400.0    500.0    630.0    800.0
  1000.0   1250.0   1600.0   2000.0   2500.0   3150.0   4000.0   5000.0   6300.0   8000.0
 10000.0  12500.0  16000.0  20000.0  25000.0  31500.0  40000.0  50000.0  63000.0  80000.0

julia> pref_freqs = pref_freqs'[:]
40-element Vector{Float64}:
    10.0
    12.5
    16.0
    20.0
    25.0
    31.5
    40.0
    50.0
    63.0
     
 16000.0
 20000.0
 25000.0
 31500.0
 40000.0
 50000.0
 63000.0
 80000.0