语音差分编码(DPCM)的实现与改进——Python实现

介绍

这是视听觉信号处理的第二个实验——语音差分编码(DPCM)。总体来讲,思路上还是比较简单的,很容易理解。如果编程能力好的话,相信很快就能完成。奈何我太菜了,写了几个晚上才算搞定。做了点扩展,添加了自己神奇的想法,在这里记录一下。先附上代码地址:视听觉信号处理实验二

DPCM 原理

DPCM 的原理很简单,就是利用信号采样点之间的关联性,即每个采样点与相邻的采样点之间的差别很小,因此,就可以利用该特性进行压缩。总地来说,就是先存储第一个采样点的数值,再存储每个采样点与前一个采样点之间的差值,作为压缩的数据。这样的话,就可以利用第一个采样点,加上差值,求出第二个采样点,然后再加差值…一直持续下去,就可以求出所有采样点的数值了,也就完成了语音还原。而且,由于没个采样点之间相差很小,因此,差值也不会很大,所以就可以利用较少的比特数来存储压缩的数据了,这样也就实现了压缩。

量化误差

由于,这里的差值可能过大,为便于存储,一般设置一个量化因子,比如,如果量化因子是100的话,差值 400 就可以映射到 4 ,这样的话压缩数据可以用更少的比特存储,但容易出现量化误差。如果将所有的差值固定在一定的范围内(比如这次实验,存储比特为4,差值范围就是 -8 到 7 ,再乘上量化因子)。因此,如果差值过大或者过小,超出范围了,就只能按照边界值来定。而这样的话,就会导致量化误差。

由于计算出来的差值需要被量化,即映射到 -7 到 8。由于量化过程中,可能会因为差值超出可以正确量化的范围,导致量化值精度不够,从而可能导致解压过程中计算出来的值与被压缩数据不同。这种量化过程中出现的误差就是量化误差。

边压缩,边解压

如果一个数据出现了量化误差,那么后面的数据在还原的过程中就会在错误的数据上进行还原,这样的话,会让之前出现的误差一直积累下去,影响后面所有的数据还原。那么该怎么解决呢,最简单的方式就是边压缩,边解压,利用上一个还原的数据再对当前的数据进行压缩,这样的话,即使产生量化误差,也只是影响一个采样点,而不会影响后续采样点的还原

Python 实现

这里并没有真正地进行解压,只是将压缩过程中的解压数组存储起来了。可以通过读取压缩文件,并根据解压公式来进行解压。在改进版的实现中写了这一过程。

import wave
import os
import numpy as np
 # 压缩文件
def compressWaveFile(wave_data) :       
    quantized_num = 100                         # 量化因子
    diff_value = []
    compressed_data = []
    decompressed_data = []
     diff_value = [wave_data[0]]
    compressed_data = [wave_data[0]]
    decompressed_data = [wave_data[0]]
    for index in range(len(wave_data)) :
        if index == 0 :
            continue
        diff_value.append(wave_data[index] - compressed_data[index - 1])
        compressed_data.append(calCompressedData(diff_value[index], quantized_num))
        decompressed_data.append(decompressed_data[index - 1] + compressed_data[index] * quantized_num)
    return compressed_data, decompressed_data
 # 计算 映射
def calCompressedData(diff_value, quantized_num) :
    if diff_value > 7 * quantized_num :
        return 7
    elif diff_value < -8 * quantized_num :
        return -8
    for i in range(16) :
        j = i - 8
        if (j - 1) * quantized_num < diff_value and diff_value <= j * quantized_num :
            return j
 for i in range(10) :
    f = wave.open("./语料/" + str(i + 1) + ".wav","rb")
    # getparams() 一次性返回所有的WAV文件的格式信息
    params = f.getparams()
    # nframes 采样点数目
    nchannels, sampwidth, framerate, nframes = params[:4]
    # readframes() 按照采样点读取数据
    str_data = f.readframes(nframes)            # str_data 是二进制字符串
    # 以上可以直接写成 str_data = f.readframes(f.getnframes())
    # 转成二字节数组形式(每个采样点占两个字节)
    wave_data = np.fromstring(str_data, dtype = np.short)
    print( "采样点数目:" + str(len(wave_data)))          #输出应为采样点数目
    f.close()
    compressed_data, decompressed_data = compressWaveFile(wave_data)
    # 写压缩文件
    with open("./压缩文件/" + str(i + 1) + ".dpc", "wb") as f :
        for num in compressed_data :
            f.write(np.int16(num))
    # 写还原文件
    with open("./还原文件/" + str(i + 1) + ".pcm", "wb") as f :
        for num in decompressed_data :
            f.write(np.int16(num)) 

改进策略

整体算法是,先对每个采样点进行取绝对值然后加一的运算,将所有采样点的值都变换到大于等于1的区间,然后对这个变换后的值取 log, 存储取完 log 之后的相邻数据之间的差值。由于这里的压缩文件需要特意存储一下每个采样点的符号(使用 1 比特),然后再进行解密。相当于每个采样点利用了加密文件的 5 个比特。

加密过程

  1. 首先,对每个采样点进行变换,变换到取绝对值加一。

  2. 计算差值,根据公式:
    差值公式
    将所有差值存到一个数组 d[0:n] 中

  3. 计算映射,将计算所得到的差值进行量化,即将差值映射到 -8 到 7 这个区间(压缩成4比特,便于存储)。将量化数据存储起来,压缩到文件中时,需要使用该信息。

  4. 然后,存储差值,且为了避免误差进行积累,一边解密,一边加密。

  5. 最后,需要计算整体采样点的符号,然后利用 1 比特进行存储,每 16 个符号为一组,组成一个 16 比特的无符号整数。(注:这一步可以跟上面的算法并行完成。)

最后计算得到的加密文件格式如下:

文件格式

解密过程:

  1. 读取压缩文件,将符号数和差值数区分开,分别存储到不同的数组中。

  2. 然后,对差值部分进行解压,利用公式:
    解压公式
    解压所得到的是原来采样点的绝对值加一,因此,先进行减一操作,然后根据对应的符号数再的符号位来判断该采样点的符号。

  3. 最后,得到一个所有采样点的数组。写入 .pcm 文件中。

改进后的Python 代码

import wave
import os
import numpy as np
import math

quantized_num = 0.12               # 量化因子
# 压缩文件
def compressWaveFile(wave_data) :       
    diff_value = []                   # 存储差值
    compressed_data = []              # 存储压缩数据
    decompressed_data = []            # 存储解压数据
    # 初始化 将第一个采样点存起来 第一个采样点不进行加密
    diff_value = [wave_data[0]]
    compressed_data = [wave_data[0]]
    decompressed_data = [wave_data[0]]
    # 压缩每个数据
    for index in range(len(wave_data)) :
        if index == 0 :
            continue
        # 做差的时候要取对数,对数的 自变量 x >= 0, 由于样本点有正有负,因此这里先取绝对值加一
        waveData_abs = abs(wave_data[index]) + 1
        decompressedData_abs = abs(decompressed_data[index - 1]) + 1 
        # 相当于对变换后的值,即取绝对值加一后的值进行加密
        diff_value.append(math.log(waveData_abs) - math.log(decompressedData_abs))
        compressed_data.append(calCompressedData(diff_value[index], quantized_num))
        # 这里进行解密,并直接将解密出来的数值进行减一操作
        de_num = math.exp(math.log(abs(decompressed_data[index - 1]) + 1) + compressed_data[index] * quantized_num) - 1
        # 判断加密之前的样本点符号是正还是负, 如果是负数,那么解密出来的也应该是负数,需要乘-1
        if wave_data[index] < 0 :
            decompressed_data.append((-1) * de_num)
            continue
        decompressed_data.append(de_num)
    return compressed_data, decompressed_data
# 将所有样本点的符号存储在
def calSig(wave_data) :
    sig_array = []
    sig_num = np.uint16(0)
    # 除去第一个采样点 每 64 个数据一组,将每组的符号位存储在一个 16 位无符号整数中
    for index in range(len(wave_data)) :
        if index == 0 :
            continue
        if index % 16 == 1 :
            if index != 1 :
                sig_array.append(sig_num)
            if wave_data[index] < 0 :
                sig_num = np.uint16(1)
            else :
                sig_num = np.uint16(0)
        if wave_data[index] < 0 :
            sig_num = np.uint16((sig_num << 1) + 1)  # 负数 左移 1 位,加 1
        else :
            sig_num = np.uint16(sig_num << 1)        # 正数 左移 1 位
        # 最后几位也要存起来
        if index == len(wave_data) - 1 :
            sig_array.append(sig_num)
    sig_array.insert(0, len(sig_array))
    # print("符号数组大小:" + str(len(sig_array)))
    return sig_array
# 计算 映射 将差值映射到 -8 到 7 之间
def calCompressedData(diff_value, quantized_num) :
    if diff_value > 7 * quantized_num :
        return 7
    elif diff_value < -8 * quantized_num :
        return -8
    for i in range(16) :
        j = i - 8
        if (j - 1) * quantized_num < diff_value and diff_value <= j * quantized_num :
            return j
# 计算信噪比
def calSignalToNoiseRatio(wave_data, decompressed_data) :
    sum_son = np.int64(0)
    sum_mum = np.int64(0)
    for i in range(len(decompressed_data)) :
        sum_son = sum_son + int(decompressed_data[i]) * int(decompressed_data[i])
        sub = decompressed_data[i] - wave_data[i]
        sum_mum = sum_mum + sub * sub
    return 10 * math.log10(float(sum_son) / float(sum_mum))
# 读取压缩文件
def readCompressedFile(compressed_str) :
    compressed_data = []          #用来存储压缩数据的数组
    # 取出前两个压缩数据,即第一个样本点 和 符号数的个数
    data_first = np.fromstring(compressed_str[0:2], dtype = np.uint16)
    compressed_data.append(data_first[0])
    data_next = np.fromstring(compressed_str[2:(data_first[0] + 1) * 2], dtype = np.uint16)
    # print("第一个数据:" + str(data_first[0]) + "\t 读取长度 :" + str(len(data_first)) + "\t" + str(len(data_next)))
    for num in data_next :
        compressed_data.append(num)
    # 第一个采样点
    com_first = np.fromstring(compressed_str[(data_first[0] + 1) * 2 : (data_first[0] + 2) * 2], dtype = np.short)
    compressed_data.append(com_first[0])
    # 去除第一个样本点,剩余所有数据都以 4 bit 存储
    compressed_str = compressed_str[(data_first[0] + 2) * 2:len(compressed_str)]
    compressed_data_append = np.fromstring(compressed_str, dtype = np.uint8)
    # 将读取出来的数据装进压缩数组中,每一个数据,拆成两个 4 bit 数
    for num in compressed_data_append :
        # 存储的时候,是转成 4 bit 无符号整数存储的, 解密时,需要转换回来
        compressed_data.append((num >> 4) - 8)
        compressed_data.append(((np.uint8(num << 4)) >> 4) - 8)
    return compressed_data
# 解密 还原文件
def decompressWaveFile(compressed_data) :
    # 取出符号数组
    sig_num = compressed_data[0]
    sig_array = compressed_data[1: sig_num + 1]
    decompressed_data = []
    # 将符号数组从压缩数组中去除
    compressed_data = compressed_data[sig_num + 1 : len(compressed_data)]
    # 将第一个采样点加入解密数组中
    decompressed_data.append(compressed_data[0])
    for i in range(len(compressed_data)) :
        if i == 0 : 
            continue
        de_num = math.exp(math.log(abs(decompressed_data[i - 1]) + 1) + compressed_data[i]* quantized_num) - 1
        # 去除第一个采样点的占位
        t = i - 1
        if np.uint16(1 << (15 - (t % 16))) & sig_array[int(t / 16)] != 0 :
            decompressed_data.append((-1) * de_num)
            continue
        decompressed_data.append(de_num)
    return decompressed_data
for i in range(10) :
    f = wave.open("./语料/" + str(i + 1) + ".wav","rb")
    # getparams() 一次性返回所有的WAV文件的格式信息
    params = f.getparams()
    # nframes 采样点数目
    nchannels, sampwidth, framerate, nframes = params[:4]
    # readframes() 按照采样点读取数据
    str_data = f.readframes(nframes)            # str_data 是二进制字符串
    # 以上可以直接写成 str_data = f.readframes(f.getnframes())
    # 转成二字节数组形式(每个采样点占两个字节)
    wave_data = np.fromstring(str_data, dtype = np.short)
    print( "采样点数目:" + str(len(wave_data)))          #输出应为采样点数目
    f.close()
    compressed_data, decompressed_data = compressWaveFile(wave_data)

    # 计算符号数组
    sig_array = calSig(wave_data)

    # 写压缩文件
    with open("./压缩文件/" + str(i + 1) + ".dpc", "wb") as f :
        # 写入样本符号
        for sig_num in sig_array : 
            f.write(np.uint16(sig_num))
        # 写入差值
        num = 0
        f.write(np.int16(compressed_data[0]))
        for j in range(len(compressed_data)) :
            # 第一个数据已经压缩
            if j == 0 :
                continue
            # 压缩数据 如果有最后一个数据没拼上一个子节,则丢弃该样本点
            elif j % 2 == 1 :
                num = np.uint8((compressed_data[j] + 8 )<< 4)
            else :
                num = np.uint8(num + np.uint8(compressed_data[j] + 8))
                f.write(num)
# 读压缩文件 解压
with open("./压缩文件/" + str(i + 1) + ".dpc", "rb") as f :
    compressed_data = readCompressedFile(f.read())
    decompressed_data = decompressWaveFile(compressed_data)
 # 测试 写压缩文件
with open("./还原文件/" + str(i + 1) + ".txt", "w") as f :
     for num in compressed_data :
         f.write(str(num) + "\n")
    # 写还原文件
    with open("./还原文件/" + str(i + 1) + ".pcm", "wb") as f :
        for num in decompressed_data :
            f.write(np.int16(num))
    print("文件 " + str(i + 1) + " 的信噪比:" + str(calSignalToNoiseRatio(wave_data, decompressed_data)))

总结

总的来说,这个实验还是挺自由的。我比较喜欢这个实验老师的风格,随意。而且老师强调,做实验不用太拘束,随便写,就跟玩一样。真的挺喜欢这个观点的,实验的主要目的就是让我们熟悉语音,掌握语音的操作和算法,总是按照那么多条条框框来,重心就很容易偏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值