Main Content

深層学習を使用したノイズに含まれる音声区間の検出

この例では、事前学習済みの深層学習モデルを使用して、SNR が低い環境内でバッチとストリーミングによる音声区間検出 (VAD) を実行します。モデルとその学習方法の詳細については、Train Voice Activity Detection in Noise Model Using Deep Learning (Audio Toolbox)を参照してください。

データの読み込みと検査

間を置きながら発声された一連の単語で構成されるオーディオ ファイルを読み取って再生します。resample (Signal Processing Toolbox)を使用して、サンプル レートが 16 kHz になるように信号をリサンプリングします。クリーンな信号に対し、detectSpeech (Audio Toolbox)を使用してグラウンド トゥルース音声領域を判定します。

fs = 16e3;
[speech,fileFs] = audioread("Counting-16-44p1-mono-15secs.wav");
speech = resample(speech,fs,fileFs);
speech = speech./max(abs(speech));

sound(speech,fs)

detectSpeech(speech,fs,Window=hamming(0.04*fs,"periodic"),MergeDistance=round(0.5*fs))

ノイズ信号を読み込み、resample (Signal Processing Toolbox)を使用してオーディオ サンプル レートに合わせます。

[noise,fileFs] = audioread("WashingMachine-16-8-mono-200secs.mp3");
noise = resample(noise,fs,fileFs);

サポート関数 mixSNR を使用して、目的の SNR レベル (dB 単位) となるように洗濯機のノイズでクリーンな音声信号を破損させます。破損させたオーディオを再生します。ネットワークは、SNR が -10 dB の条件で学習させました。

SNR = -10;
noisySpeech = mixSNR(speech,noise,SNR);

sound(noisySpeech,fs)

アルゴリズムに基づく VAD であるdetectSpeech (Audio Toolbox)は、このようなノイズが多い条件ではうまく機能しません。

detectSpeech(noisySpeech,fs,Window=hamming(0.04*fs,"periodic"),MergeDistance=round(0.5*fs))

事前学習済みのネットワークのダウンロード

事前学習済みのネットワークと構成済みのaudioFeatureExtractor (Audio Toolbox)オブジェクトをダウンロードして読み込みます。このネットワークは、audioFeatureExtractor オブジェクトから出力された特徴を使用して、SNR が低い環境内で音声を検出するよう学習されています。

downloadFolder = matlab.internal.examples.downloadSupportFile("audio/examples","vadbilsmtnet.zip");
dataFolder = tempdir;
netFolder = fullfile(dataFolder,"vadbilsmtnet");
unzip(downloadFolder,netFolder)
pretrainedNetwork = load(fullfile(netFolder,"voiceActivityDetectionExample.mat"));

afe = pretrainedNetwork.afe;
net = pretrainedNetwork.speechDetectNet;

audioFeatureExtractor オブジェクトは、ウィンドウ間のオーバーラップが 128 サンプルである 256 サンプルのウィンドウから特徴を抽出するように構成されています。サンプル レートが 16 kHz の場合、オーバーラップが 8 ms である 16 ms のウィンドウから特徴が抽出されます。audioFeatureExtractor オブジェクトは、各ウィンドウから 9 つの特徴 (スペクトル重心、スペクトル クレスト、スペクトル エントロピー、スペクトル フラックス、スペクトル尖度、スペクトル ロールオフ ポイント、スペクトル歪度、スペクトル傾斜、および高調波比) を抽出します。

afe
afe = 
  audioFeatureExtractor with properties:

   Properties
                     Window: [256×1 double]
              OverlapLength: 128
                 SampleRate: 16000
                  FFTLength: []
    SpectralDescriptorInput: 'linearSpectrum'
        FeatureVectorLength: 9

   Enabled Features
     spectralCentroid, spectralCrest, spectralEntropy, spectralFlux, spectralKurtosis, spectralRolloffPoint
     spectralSkewness, spectralSlope, harmonicRatio

   Disabled Features
     linearSpectrum, melSpectrum, barkSpectrum, erbSpectrum, mfcc, mfccDelta
     mfccDeltaDelta, gtcc, gtccDelta, gtccDeltaDelta, spectralDecrease, spectralFlatness
     spectralSpread, pitch, zerocrossrate, shortTimeEnergy


   To extract a feature, set the corresponding property to true.
   For example, obj.mfcc = true, adds mfcc to the list of enabled features.

このネットワークは、2 つの双方向 LSTM 層から成り、それぞれ 200 個の隠れユニットと 1 つの分類出力をもちます。この出力は、音声区間が検出されなかった場合はクラス 0 を返し、音声区間が検出された場合はクラス 1 を返します。

net.Layers
ans = 
  5×1 Layer array with layers:

     1   'sequenceinput'   Sequence Input    Sequence input with 9 dimensions
     2   'biLSTM_1'        BiLSTM            BiLSTM with 200 hidden units
     3   'biLSTM_2'        BiLSTM            BiLSTM with 200 hidden units
     4   'fc'              Fully Connected   2 fully connected layer
     5   'softmax'         Softmax           softmax

音声区間検出の実行

音声データから特徴を抽出して標準化します。時間が列に沿うように特徴の向きを変更します。

features = extract(afe,noisySpeech);
features = (features - mean(features,1))./std(features,[],1);
features = features';

音声検出ネットワークに特徴を渡し、各特徴ベクトルが音声フレームに属するかどうかを分類させます。

scores = predict(net,features.');
decisionsCategorical = scores2label(scores,categorical([0 1]));

各判定は、audioFeatureExtractor (Audio Toolbox)によって解析された解析ウィンドウに対応します。オーディオ サンプルと 1 対 1 で対応するように、判定を複製します。出力引数なしでdetectSpeech (Audio Toolbox)を使用し、グラウンド トゥルースをプロットします。signalMask (Signal Processing Toolbox)およびplotsigroi (Signal Processing Toolbox)を使用して、予測された VAD をプロットします。

decisions = (double(decisionsCategorical) - 1);
decisionsPerSample = [decisions(1:round(numel(afe.Window)/2));repelem(decisions,numel(afe.Window)-afe.OverlapLength,1)];

tiledlayout(2,1)

nexttile
detectSpeech(speech,fs,Window=hamming(0.04*fs,"periodic"),MergeDistance=round(0.5*fs))
title("Ground Truth VAD")
xlabel("")

nexttile
mask = signalMask(decisionsPerSample,SampleRate=fs,Categories="Activity");
plotsigroi(mask,noisySpeech,true)
title("Predicted VAD")

ストリーミングによる音声区間検出の実行

audioFeatureExtractor (Audio Toolbox)オブジェクトはバッチ処理を目的としており、呼び出し間で状態を保持しません。ストリーミングに適した特徴抽出器を作成するには、generateMATLABFunction (Audio Toolbox)を使用します。classifyAndUpdateStateを使用すると、ストリーミングのコンテキストで学習済みの VAD ネットワークを使用することができます。

generateMATLABFunction(afe,"featureExtractor",IsStreaming=true)

ストリーミング環境のシミュレーションを行うには、音声とノイズ信号を WAV ファイルとして保存します。ストリーミング入力のシミュレーションを行うには、dsp.AudioFileReader (DSP System Toolbox)を使用してファイルからフレームを読み取り、目的とする SNR でそれらを混合します。音声ソースがマイクの場合は、audioDeviceReader (Audio Toolbox)を使うこともできます。

audiowrite("Speech.wav",speech,fs)
audiowrite("Noise.wav",noise,fs)

ノイズに含まれるストリーミング音声区間の検出のデモに使用するパラメーターを定義します。

  • signal - 信号ソース。事前録音済みの音声ファイルまたはマイクとして指定します。

  • noise - ノイズ ソース。信号と混ぜ合わせるノイズ音のファイルとして指定します。

  • SNR - 信号とノイズを混ぜ合わせた後の S/N 比。dB 単位で指定します。

  • testDuration - テスト期間。秒単位で指定します。

  • playbackSource - 再生ソース。元のクリーンな信号、ノイズを含む信号、または検出された音声として指定します。オーディオをスピーカーで再生するには、audioDeviceWriter (Audio Toolbox)オブジェクトを使用します。

signal = "Speech.wav";
noise = "Noise.wav";
SNR = -10; % dB

testDuration = 20; % seconds
playbackSource = "noisy";

サポート関数 streamingDemo を呼び出し、ストリーミング オーディオに対する VAD ネットワークの性能を観察します。パラメーターをライブ コントロールで設定しても、ストリーミングの例は中断されません。ストリーミング デモが完了した後、デモのパラメーターに変更を加えてストリーミング デモを再度実行できます。

streamingDemo(net,afe, ...
    signal,noise,SNR, ...
    testDuration,playbackSource);

参考文献

[1] Warden P. "Speech Commands: A public dataset for single-word speech recognition", 2017. Available from https://storage.googleapis.com/download.tensorflow.org/data/speech_commands_v0.01.tar.gz. Copyright Google 2017. Speech Commands Dataset は、Creative Commons Attribution 4.0 license に従ってライセンスされています。

サポート関数

ストリーミングのデモ

function streamingDemo(net,afe,signal,noise,SNR,testDuration,playbackSource)
% streamingDemo(net,afe,signal,noise,SNR,testDuration,playbackSource) runs
% a real-time VAD demo.

% Create dsp.AudioFileReader objects to read speech and noise files frame
% by frame. If the speech signal is specified as Microphone, use an
% audioDeviceReader as the source.
if strcmpi(signal,"Microphone")
    speechReader = audioDeviceReader(afe.SampleRate);
else
    speechReader = dsp.AudioFileReader(signal,PlayCount=inf);
end
noiseReader = dsp.AudioFileReader(noise,PlayCount=inf,SamplesPerFrame=speechReader.SamplesPerFrame);
fs = speechReader.SampleRate;

% Create a dsp.MovingStandardDeviation object and a dsp.MovingAverage
% object. You will use these to determine the standard deviation and mean
% of the audio features for standardization. The statistics should improve
% over time.
movSTD = dsp.MovingStandardDeviation(Method="Exponential weighting",ForgettingFactor=1);
movMean = dsp.MovingAverage(Method="Exponential weighting",ForgettingFactor=1);

% Create a dsp.MovingMaximum object. You will use it to standardize the
% audio.
movMax = dsp.MovingMaximum(SpecifyWindowLength=false);

% Create a dsp.MovingRMS object. You will use this to determine the signal
% and noise mix at the desired SNR. This object is only useful for example
% purposes where you are artificially adding noise.
movRMS = dsp.MovingRMS(Method="Exponential weighting",ForgettingFactor=1);

% Create three dsp.AsyncBuffer objects. One to buffer the input audio, one
% to buffer the extracted features, and one to buffer the output audio so
% that VAD decisions correspond to the audio signal. The output buffer is
% only necessary for visualizing the decisions in real time.
audioInBuffer = dsp.AsyncBuffer(2*speechReader.SamplesPerFrame);
featureBuffer = dsp.AsyncBuffer(ceil(2*speechReader.SamplesPerFrame/(numel(afe.Window)-afe.OverlapLength)));
audioOutBuffer = dsp.AsyncBuffer(2*speechReader.SamplesPerFrame);

% Create a time scope to visualize the original speech signal, the noisy
% signal that the network is applied to, and the decision output from the
% network.
scope = timescope(SampleRate=fs, ...
    TimeSpanSource="property", ...
    TimeSpan=3, ...
    BufferLength=fs*3*3, ...
    TimeSpanOverrunAction="Scroll", ...
    AxesScaling="updates", ...
    MaximizeAxes="on", ...
    AxesScalingNumUpdates=20, ...
    NumInputPorts=3, ...
    LayoutDimensions=[3,1], ...
    ChannelNames=["Noisy Speech","Clean Speech (Original)","Detected Speech"], ...
    ...
    ActiveDisplay = 1, ...
    ShowGrid=true, ...
    ...
    ActiveDisplay = 2, ...
    ShowGrid=true, ...
    ...
    ActiveDisplay=3, ...
    ShowGrid=true); 
setup(scope,{1,1,1})

% Create an audioDeviceWriter object to play either the original or noisy
% audio from your speakers.
deviceWriter = audioDeviceWriter(SampleRate=fs);

% Initialize variables used in the loop.
windowLength = numel(afe.Window);
hopLength = windowLength - afe.OverlapLength;

% Run the streaming demonstration.
loopTimer = tic;
while toc(loopTimer) < testDuration

    % Read a frame of the speech signal and a frame of the noise signal
    speechIn = speechReader();
    noiseIn = noiseReader();

    % Mix the speech and noise at the specified SNR
    energy = movRMS([speechIn,noiseIn]);
    noiseGain = 10^(-SNR/20) * energy(end,1) / energy(end,2);
    noisyAudio = speechIn + noiseGain*noiseIn;

    % Update a running max to scale the audio
    myMax = movMax(abs(noisyAudio));
    noisyAudio = noisyAudio/myMax(end);

    % Write the noisy audio and speech to buffers
    write(audioInBuffer,[noisyAudio,speechIn]);

    % If enough samples are in the audio buffer to calculate a feature
    % vector, read the samples, normalize them, extract the feature
    % vectors, and write the latest feature vector to the features buffer.
    while (audioInBuffer.NumUnreadSamples >= hopLength)
        x = read(audioInBuffer,numel(afe.Window),afe.OverlapLength);
        write(audioOutBuffer,x(end-hopLength+1:end,:));
        noisyAudio = x(:,1);
        features = featureExtractor(noisyAudio);
        write(featureBuffer,features');
    end

    if featureBuffer.NumUnreadSamples >= 1
        % Read the audio data corresponding to the number of unread
        % feature vectors.
        audioHop = read(audioOutBuffer,featureBuffer.NumUnreadSamples*hopLength);

        % Read all unread feature vectors.
        features = read(featureBuffer);

        % Use only the new features to update the standard deviation and
        % mean. Normalize the features.
        rmean = movMean(features);
        rstd = movSTD(features);
        features = (features - rmean(end,:)) ./ rstd(end,:);

        % Network inference
        [score,state] = predict(net,features); 
        net.State = state; 
        [~,decision] = max(score,[],2);
        decision = decision-1;

        % Convert the decisions per feature vector to decisions per sample
        decision = repelem(decision,hopLength,1);

        % Apply a mask to the noisy speech for visualization
        vadResult = audioHop(:,1);
        vadResult(decision==0) = 0;

        % Listen to the speech or speech+noise
        switch playbackSource
            case "clean"
                deviceWriter(audioHop(:,2));
            case "noisy"
                deviceWriter(audioHop(:,1));
            case "detectedSpeech"
                deviceWriter(vadResult);
        end

        % Visualize the speech+noise, the original speech, and the voice
        % activity detection.
        scope(audioHop(:,1),audioHop(:,2),vadResult)

    end
end
end

混合 SNR

function [noisySignal,requestedNoise] = mixSNR(signal,noise,ratio)
% [noisySignal,requestedNoise] = mixSNR(signal,noise,ratio) returns a noisy
% version of the signal, noisySignal. The noisy signal has been mixed with
% noise at the specified ratio in dB.

numSamples = size(signal,1);

% Convert noise to mono
noise = mean(noise,2);

% Trim or expand noise to match signal size
if size(noise,1)>=numSamples
    % Choose a random starting index such that you still have numSamples
    % after indexing the noise.
    start = randi(size(noise,1) - numSamples + 1);
    noise = noise(start:start+numSamples-1);
else
    numReps = ceil(numSamples/size(noise,1));
    temp = repmat(noise,numReps,1);
    start = randi(size(temp,1) - numSamples - 1);
    noise = temp(start:start+numSamples-1);
end

signalNorm = norm(signal);
noiseNorm = norm(noise);

goalNoiseNorm = signalNorm/(10^(ratio/20));
factor = goalNoiseNorm/noiseNorm;

requestedNoise = noise.*factor;
noisySignal = signal + requestedNoise;

noisySignal = noisySignal./max(abs(noisySignal));
end

参考

(Audio Toolbox) | (Audio Toolbox)

関連するトピック