Skip to content

Instantly share code, notes, and snippets.

@shantiphula
Last active January 22, 2021 04:17
Show Gist options
  • Save shantiphula/b54c935dd1ea742350874dcd696a6557 to your computer and use it in GitHub Desktop.
Save shantiphula/b54c935dd1ea742350874dcd696a6557 to your computer and use it in GitHub Desktop.
「調和純正律で遊ぼう」WAVEファイルから基音周波数を検出するRubyによるプログラム例

調和純正律で遊ぼう 〜 【発展編】周波数の解析」で紹介した、音声ファイル(WAVEファイル)を離散フーリエ解析し、基音周波数の取り出しを行うRubyプログラムの断片です。

  • audio_fund_freq_extractor.rb … 処理を行う本体です。
  • audio_fund_freq_extractor_spec.rb … テスト兼使用例です。
  • sample-A2.mp3 … テストで用いるサンプル音声(事前にwavファイルへ変換が必要)

テストの実行方法

# sample-A2.mp3 を wav ファイルに変換
sox sample-A2.mp3 sample-A2.wav

bundle
bundle exec rspec .
*.wav
Gemfile.lock
require "wavefile"
require "numru/fftw3"
# 音声ファイルを離散フーリエ変換により周波数解析し、その中から最も強い基音の周波数を取り出す。
#
# Author: Shanti Phula https://shanti-phula.net/
# License: CC0
#
# @note 窓関数は適用しない。(つまり矩形窓となる)
# @note 先頭から window_size_nsamples サンプルを取得して解析する。足りないサンプル分は後ろにゼロを加える。
# @note ステレオ音声はモノラルミックスダウンして解析する。
class AudioFundFreqExtractor
# 周波数の小数点以下精度
HZ_PRECISION = 3
# 振幅値の小数点以下精度
AMP_PRECISION = 6
# 基音の周波数とみなす範囲(単位:セント)
NOTE_FREQ_MATCH_RANGE_BY_CENT = -100..100
# 周波数成分
class FreqComponent
# @return [Float]
attr_accessor :hz
# @return [Float]
attr_accessor :amplitude
def initialize(hz, amplitude)
@hz = hz
@amplitude = amplitude
end
end
class Result
# @return [FreqComponent] 基音の周波数成分
attr_accessor :fund_freq_component
# @return [Array<FreqComponent>] すべての周波数成分
attr_accessor :freq_components
end
# @return [String] 解析するWAVEファイル
attr_accessor :wave_file
# @return [Integer] FFTのウィンドウサイズ
attr_accessor :window_size_nsamples
# @return [Float] 解析するWAVEファイルの音に対応した平均律における周波数
attr_accessor :fund_freq_hz
# @return [Result]
def perform
Result.new.tap { |result|
load_samples
result.freq_components = fft
result.fund_freq_component = detect_fund_freq_component(result.freq_components)
}
end
private
# 最も強い基音の周波数成分を取り出す。
def detect_fund_freq_component(all_components)
all_components.sort_by { |fc|
-(fc.amplitude)
}.find { |fc|
cent = 1200 * Math.log2(fc.hz / fund_freq_hz)
NOTE_FREQ_MATCH_RANGE_BY_CENT.cover?(cent)
}
end
# 離散フーリエ変換を行う。
def fft
# ウィンドウサイズ分のサンプルを取り出す。
samples_to_analyze = @samples[0, window_size_nsamples]
# サンプルが足りなければ、不足部分を0で埋める。
# (ゼロパディング)
if samples_to_analyze.size < window_size_nsamples
samples_to_analyze.fill(0, samples_to_analyze.size...window_size_nsamples)
end
# サンプルを収めた1次元行列を作る。
na = NArray.to_na(samples_to_analyze)
# FFTを実行する。
fa = NumRu::FFTW3.fft_fw(na, 0)
# 要素は複素数なので、振幅(複素数平面上の原点からの距離)を取り出す。
fa = fa.abs
# 離散フーリエ変換の結果は、
#
# fa[0] = DC(=0Hzのこと)の振幅
# ...
# fa[ウィンドウサイズ/2 - 1] = ナイキスト周波数の振れ幅 --A
# ...
# fa[ウィンドウサイズ - 1] = DC(=0Hzのこと)の振幅
#
# というふうに中央で鏡像になっているので、添字の最大値は、上Aでよい。
#
# ※ナイキスト周波数:あるサンプリングレートで表現可能な最大周波数。SR/2。
dft_n_max = window_size_nsamples / 2 - 1
(0..(dft_n_max)).map do |n|
# 周波数は、 (SR / ウィンドウサイズ) * n で求めることができる。
hz = (@sample_rate.to_f / window_size_nsamples) * n
amplitude = fa[n]
FreqComponent.new(hz.round(HZ_PRECISION), amplitude.round(AMP_PRECISION))
end
end
# 音声ファイルからサンプル(離散信号)を読み込む。
def load_samples
native_format = nil
WaveFile::Reader.new(wave_file) do |reader|
native_format = reader.native_format
end
target_format = WaveFile::Format.new(
:mono, :float_64, native_format.sample_rate
)
WaveFile::Reader.new(wave_file, target_format) do |reader|
@samples = []
reader.each_buffer do |buffer|
@samples.concat buffer.samples
end
end
@sample_rate = native_format.sample_rate
@duration = @samples.size / @sample_rate
end
end
require "bundler/setup"
Bundler.require
require_relative "audio_fund_freq_extractor.rb"
RSpec.describe 'AudioFundFreqExtractor' do
let :sample_wavfile do
wavfile = File.expand_path("../sample-A2.wav", __FILE__)
unless File.exist?(wavfile)
raise "#{wavfile} がありません。mp3ファイルから変換して作成してください。"
end
wavfile
end
it '音声ファイルを離散フーリエ変換により周波数解析し、その中から最も強い基音の周波数を取り出す。' do
result = AudioFundFreqExtractor.new.tap { |it|
it.wave_file = sample_wavfile
it.window_size_nsamples = 65536
it.fund_freq_hz = 110 # 平均律におけるA2の周波数
}.perform
expect(result.fund_freq_component.hz).to eq(109.012)
expect(result.freq_components[0]).to be_kind_of(AudioFundFreqExtractor::FreqComponent)
end
end
source "https://rubygems.org"
gem "wavefile", "~> 1.0"
gem "ruby-fftw3", "~> 1.0"
gem "rspec", "~> 3.0"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment