将PCM波形数据转换为numpy数组,反之亦然。

12

情况

我正在使用WebRTC-VAD,这是一个Python适配器,用于使用WebRTC中的VAD(语音活动检测)。GitHub仓库中的示例实现使用Python的wave模块从文件中读取PCM数据。请注意,根据注释,该模块仅适用于单声道音频和采样率为8000、16000或32000 Hz。

我想做什么

从任意音频文件(MP3和WAV文件)中读取音频数据,并将它们转换为WebRTC-VAD使用的PCM表示形式,应用WebRTC-VAD以检测语音活动,最后通过从PCM数据再次产生Numpy-Arrays来处理结果,因为在使用Librosa时它们最容易使用。

我的问题

当使用wave模块时,WebRTC-VAD模块才能正常工作。该模块将PCM数据返回为bytes对象。如果使用例如使用librosa.load(...)获得的Numpy数组,则无法正常工作。我还没有找到在两种表示之间进行转换的方法。

我已经做了什么

我已经编写了以下函数,可以从音频文件中读取音频数据并自动转换:

使用Librosa的通用函数来读取/转换任何音频数据(--> 返回Numpy数组):

def read_audio(file_path, sample_rate=None, mono=False):       
    return librosa.load(file_path, sr=sample_rate, mono=mono)

读取任意数据作为PCM数据的函数(--> 返回字节):

def read_audio_vad(file_path):
    audio, rate = librosa.load(file_path, sr=16000, mono=True)
    tmp_file = 'tmp.wav'
    sf.write(tmp_file, audio, rate, subtype='PCM_16')
    audio, rate = read_pcm16_wave(tmp_file)
    remove(tmp_file)
    return audio, rate

def read_pcm16_wave(file_path):    
    with wave.open(file_path, 'rb') as wf:
        sample_rate = wf.getframerate()
        pcm_data = wf.readframes(wf.getnframes())
        return pcm_data, sample_rate

如您所见,我首先通过使用Librosa读取/转换音频数据来绕过一些步骤。这样做是必要的,因为我需要能够从带有任意编码的MP3文件或WAV文件中读取,并使用Librosa自动重新采样为16kHz单声道格式。然后我将数据写入临时文件。在删除该文件之前,我再次读取其中的内容,但这次使用“wave”模块。这会给我提供PCM数据。
我现在有以下代码来提取语音活动并生成Numpy数组:
def webrtc_voice(audio, rate):
    voiced_frames = webrtc_split(audio, rate)
    tmp_file = 'tmp.wav'
    for frames in voiced_frames:
        voice_audio = b''.join([f.bytes for f in frames])
        write_pcm16_wave(tmp_file, voice_audio, rate)
        voice_audio, rate = read_audio(tmp_file)
        remove(tmp_file)

        start_time = frames[0].timestamp
        end_time = (frames[-1].timestamp + frames[-1].duration)
        start_frame = int(round(start_time * rate / 1e3)) 
        end_frame = int(round(end_time * rate / 1e3)) 
        yield voice_audio, rate, start_frame, end_frame

def write_pcm16_wave(path, audio, sample_rate):
    with wave.open(path, 'wb') as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(sample_rate)
        wf.writeframes(audio)

如您所见,我再次采用暂存文件的方法先写入PCM数据,然后再使用Librosa将临时文件读出并转换为Numpy数组。webrtc_split函数是从示例实现中提取的,仅稍作修改。为了完整起见,在此贴出:

def webrtc_split(audio, rate, aggressiveness=3, frame_duration_ms=30, padding_duration_ms=300):
    vad = Vad(aggressiveness)

    num_padding_frames = int(padding_duration_ms / frame_duration_ms)
    ring_buffer = collections.deque(maxlen=num_padding_frames)
    triggered = False

    voiced_frames = []
    for frame in generate_frames(audio, rate):
        is_speech = vad.is_speech(frame.bytes, rate)

        if not triggered:
            ring_buffer.append((frame, is_speech))
            num_voiced = len([f for f, speech in ring_buffer if speech])
            if num_voiced > 0.9 * ring_buffer.maxlen:
                triggered = True
                for f, s in ring_buffer:
                    voiced_frames.append(f)
                ring_buffer.clear()
        else:
            voiced_frames.append(frame)
            ring_buffer.append((frame, is_speech))
            num_unvoiced = len([f for f, speech in ring_buffer if not speech])
            if num_unvoiced > 0.9 * ring_buffer.maxlen:
                triggered = False
                yield voiced_frames
                ring_buffer.clear()
                voiced_frames = []
    if voiced_frames:
        yield voiced_frames


class Frame(object):
    """
    object holding the audio signal of a fixed time interval (30ms) inside a long audio signal
    """

    def __init__(self, bytes, timestamp, duration):
        self.bytes = bytes
        self.timestamp = timestamp
        self.duration = duration


def generate_frames(audio, sample_rate, frame_duration_ms=30):
    frame_length = int(sample_rate * frame_duration_ms / 1000) * 2
    offset = 0
    timestamp = 0.0
    duration = (float(frame_length) / sample_rate)
    while offset + frame_length < len(audio):
        yield Frame(audio[offset:offset + frame_length], timestamp, duration)
        timestamp += duration
        offset += frame_length

我的问题

我使用wave模块编写/读取临时文件,并使用Librosa读取/写入这些文件以获取Numpy数组,但我觉得这种实现过于复杂。尽管我花了一整天的时间研究此事,但我没有找到直接在两个编码之间转换的方法。我承认我并不完全理解PCM和WAVE文件的所有细节,使用16/24/32位PCM数据或字节序的影响。我希望我上面的解释足够详细,但不会太多。有没有更简单的方法在内存中在这两个表示之间进行转换?

1个回答

8

看起来WebRTC-VAD和其Python封装py-webrtcvad需要音频数据为16位PCM小端格式,这是WAV文件中最常见的存储格式。

librosa及其底层I/O库pysoundfile总是返回范围在[-1.0, 1.0]之间的浮点数数组。要将其转换为包含16位PCM的字节,您可以使用以下float_to_pcm16函数。

def float_to_pcm16(audio):
    import numpy

    ints = (audio * 32767).astype(numpy.int16)
    little_endian = ints.astype('<u2')
    buf = little_endian.tostring()
    return buf


def read_pcm16(path):
    import soundfile

    audio, sample_rate = soundfile.read(path)
    assert sample_rate in (8000, 16000, 32000, 48000)
    pcm_data = float_to_pcm16(audio)
    return pcm_data, sample_rate

1
ndarray.tostring() 已经废弃,请使用tobytes()代替。 - tjysdsg
1
应该是“(audio * 32768)”,而不是“(audio * 32767)” - Chris Wong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接