这是视听觉信号处理的第二个实验——语音差分编码(DPCM)。总体来讲,思路上还是比较简单的,很容易理解。如果编程能力好的话,相信很快就能完成。奈何我太菜了,写了几个晚上才算搞定。做了点扩展,添加了自己神奇的想法,在这里记录一下。先附上代码地址:视听觉信号处理实验二
DPCM 的原理很简单,就是利用信号采样点之间的关联性,即每个采样点与相邻的采样点之间的差别很小,因此,就可以利用该特性进行压缩。总地来说,就是先存储第一个采样点的数值,再存储每个采样点与前一个采样点之间的差值,作为压缩的数据。这样的话,就可以利用第一个采样点,加上差值,求出第二个采样点,然后再加差值…一直持续下去,就可以求出所有采样点的数值了,也就完成了语音还原。而且,由于没个采样点之间相差很小,因此,差值也不会很大,所以就可以利用较少的比特数来存储压缩的数据了,这样也就实现了压缩。
由于,这里的差值可能过大,为便于存储,一般设置一个量化因子,比如,如果量化因子是100的话,差值 400 就可以映射到 4 ,这样的话压缩数据可以用更少的比特存储,但容易出现量化误差。如果将所有的差值固定在一定的范围内(比如这次实验,存储比特为4,差值范围就是 -8 到 7 ,再乘上量化因子)。因此,如果差值过大或者过小,超出范围了,就只能按照边界值来定。而这样的话,就会导致量化误差。
由于计算出来的差值需要被量化,即映射到 -7 到 8。由于量化过程中,可能会因为差值超出可以正确量化的范围,导致量化值精度不够,从而可能导致解压过程中计算出来的值与被压缩数据不同。这种量化过程中出现的误差就是量化误差。
如果一个数据出现了量化误差,那么后面的数据在还原的过程中就会在错误的数据上进行还原,这样的话,会让之前出现的误差一直积累下去,影响后面所有的数据还原。那么该怎么解决呢,最简单的方式就是边压缩,边解压,利用上一个还原的数据再对当前的数据进行压缩,这样的话,即使产生量化误差,也只是影响一个采样点,而不会影响后续采样点的还原。
这里并没有真正地进行解压,只是将压缩过程中的解压数组存储起来了。可以通过读取压缩文件,并根据解压公式来进行解压。在改进版的实现中写了这一过程。
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 个比特。
首先,对每个采样点进行变换,变换到取绝对值加一。
计算映射,将计算所得到的差值进行量化,即将差值映射到 -8 到 7 这个区间(压缩成4比特,便于存储)。将量化数据存储起来,压缩到文件中时,需要使用该信息。
然后,存储差值,且为了避免误差进行积累,一边解密,一边加密。
最后,需要计算整体采样点的符号,然后利用 1 比特进行存储,每 16 个符号为一组,组成一个 16 比特的无符号整数。(注:这一步可以跟上面的算法并行完成。)
最后计算得到的加密文件格式如下:
读取压缩文件,将符号数和差值数区分开,分别存储到不同的数组中。
然后,对差值部分进行解压,利用公式:
解压所得到的是原来采样点的绝对值加一,因此,先进行减一操作,然后根据对应的符号数再的符号位来判断该采样点的符号。
最后,得到一个所有采样点的数组。写入 .pcm 文件中。
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)))
总的来说,这个实验还是挺自由的。我比较喜欢这个实验老师的风格,随意。而且老师强调,做实验不用太拘束,随便写,就跟玩一样。真的挺喜欢这个观点的,实验的主要目的就是让我们熟悉语音,掌握语音的操作和算法,总是按照那么多条条框框来,重心就很容易偏。