项目5 PROJECT 5 歌曲人声分离 本项目通过TensorFlow构建BiLSTM,针对唱歌软件的基本伴奏资源,使用STFT(Short Time Fourier TransForm,短时傅里叶变换)进行处理,实现原曲获得音频质量较好的伴奏和纯人声音轨。 5.1总体设计 本部分包括系统整体结构图和系统流程图。 5.1.1系统整体结构图 系统整体结构如图51所示。 图51系统整体结构图 5.1.2系统流程图 系统流程如图52所示。 图52系统流程图 5.2运行环境 本部分包括Python环境、TensorFlow环境和Jupyter Notebook环境。 5.2.1Python环境 需要Python 3.6及以上配置,在Windows环境下推荐下载Anaconda完成Python所需的配置,下载地址为https://www.anaconda.com/,默认下载Python 3.7版本。打开Anaconda Prompt,安装开源的音频处理库librosa,输入命令: pip install librosa 安装完毕。 5.2.2TensorFlow环境 (1) 打开Anaconda Prompt,输入清华仓库镜像,输入命令: conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config –set show_channel_urls yes (2) 创建Python 3.6环境,默认3.7版本和低版本TensorFlow存在不兼容问题,所以建立Python 3.6版本,输入命令: conda create -n python36 python=3.6 依据给出的相关提示,逐步安装。 (3) 在Anaconda Prompt中激活创建的虚拟环境,输入命令: activate python36 (4) 安装CPU版本的TensorFlow,输入命令: conda install –upgrade --ignore-installed tensorflow 安装完毕。 5.2.3Jupyter Notebook环境 (1) 首先打开Anaconda Prompt,激活安装TensoFlow的虚拟环境,输入命令: activate python36 (2) 安装ipykernel,输入命令: conda install ipykernel (3) 将此环境写入Jupyter Notebook的Kernel中,输入命令: python -m ipykernel install --name python36 --display-name "tensorflow(python36)" (4) 打开Jupyter Notebook,进入工作目录后,输入命令: jupyter notebook 安装完毕。 5.3模块实现 本项目包括5个模块: 数据准备、数据预处理、模型构建、模型训练及保存、模型测试,下面分别给出各模块的功能介绍及相关代码,目录结构如图53所示。 图53目录结构 (1) dataset: 放置训练数据和验证数据。 (2) image: 放置GUI制作所需要的图片素材。 (3) model: 存储训练的模型。 (4) gui.py: 模型的使用和GUI界面。 (5) model.py: 构建网络模型。 (6) train.py: 训练文件,包括数据的预处理、模型的训练及保存。 (7) utils.py: 存放需要的工具函数。 5.3.1数据准备 数据集适用于歌曲旋律提取、歌声分离、人声检测,如图54所示。 图54数据集结构 Wavfile文件夹包含1150个歌曲文件,全部采用双声道.wav格式。左声道为人声,右声道为伴奏。UndividedWavfile文件夹中包含了110个歌曲文件,文件格式为单声道.wav。 Wavfile文件夹中的所有数据作为训练集,UndividedWavfile文件夹中的所有数据作为验证集。将MIR1K下载解压至工作目录中的dataset文件夹中,完成数据集的准备工作。数据集下载地址为http://mirlab.org/dataset/public/MIR1K_for_MIREX.rar。 5.3.2数据预处理 数据预处理主要完成如下功能: 载入文件列表; 将列表中的.wav文件读取到内存中,并分解为单声道原曲、单声道伴奏、单声道纯人声; 把处理好的音频进行短时傅里叶变化。具体包括载入数据集和验证集文件列表、读入文件并进行分离、.wav文件进行STFT变换、处理频域文件、相关路径与参数的设定。 1. 载入数据集和验证集文件列表 将dataset文件夹中的训练集和验证集文件建立索引,保存到列表中,进行数据预处理。 #导入操作系统模块 import os #导入文件夹中的数据 def load_file(dir): file_list = list() for filename in os.listdir(dir): file_list.append(os.path.join(dir, filename)) #将文件夹中的信息存入file_list列表中 return file_list #设置数据集路径 dataset_train_dir = './dataset/MIR-1K/Wavfile' dataset_validate_dir = './dataset/MIR-1K/UndividedWavfile' train_file_list = load_file(dataset_train_dir) valid_file_list = load_file(dataset_validate_dir) 2. 读入文件并进行分离 将列表中的文件读取到内存中,进行音频的分离操作。原数据集中的音频文件都是双声道文件。其中左声道为纯伴奏声轨,右声道为纯人声轨,这里将左右声道和原文件分别保存。 #导入音频处理库 import librosa #数据集的采样率 mir1k_sr = 16000 #将file_list中的文件读入内存 def load_wavs(filenames, sr): wavs_mono = list() wavs_music = list() wavs_voice = list() #读取.wav文件(要求源文件是双声道的音频文件,一个声道是纯伴奏,另一个声道是纯人声) #将音频转换成单声道,存入 wavs_mono #将纯伴奏存入wavs_music, #将纯人声存入wavs_voice for filename in filenames: #librosa.load函数:根据输入的采样率将文件读入 wav, _ = librosa.load(filename, sr=sr, mono=False) assert(wav.ndim== 2) and (wav.shape[0]==2), '要求WAV文件有两个声道!' #librosa.to_mono:将双声道转为单声道 wav_mono = librosa.to_mono(wav) * 2 wav_music = wav[0, :] wav_voice = wav[1, :] wavs_mono.append(wav_mono) wavs_music.append(wav_music) wavs_voice.append(wav_voice) #返回单声道原歌曲、纯伴奏、纯人声 return wavs_mono, wavs_music, wavs_voice #导入训练数据集的.wav音频数据 #wavs_mono_train存的是单声道音频,wavs_music_train 存的是纯伴奏,wavs_voice_train 存的是纯人声 wavs_mono_train, wavs_music_train, wavs_voice_train = load_wavs(filenames=train_file_list, sr=mir1k_sr) #导入验证集的.wav数据 wavs_mono_valid, wavs_music_valid, wavs_voice_valid = load_wavs(filenames=valid_file_list, sr=mir1k_sr) 3. wav文件进行STFT变换 将读入的验证集和训练集音频文件从时域转化为频域。调用自定义的频域转换函数wavs_to_specs()。但是训练集的数量较大,如果一次性全部转换,会导致内存占用过多。因此,每次随机抽取一个batch_size大小的文件进行频域转换。 #导入numpy数学库 import numpy #通过短时傅里叶变换将声音转到频域 #三组数据分别进行转换 def wavs_to_specs(wavs_mono, wavs_music, wavs_voice, n_fft=1024, hop_length=None): stfts_mono = list() stfts_music = list() stfts_voice = list() for wav_mono, wav_music, wav_voice in zip(wavs_mono, wavs_music, wavs_voice): #在librosa0.7.1及以上版本中,单声道音频文件必须为fortran-array格式,才能送入librosa.stft()进行处理 #使用numpy.asfortranarray()函数,对单声道音频文件进行上述格式转换 stft_mono = librosa.stft((numpy.asfortranarray(wav_mono)), n_fft=n_fft, hop_length=hop_length) stft_music = librosa.stft((numpy.asfortranarray(wav_music)), n_fft=n_fft, hop_length=hop_length) stft_voice = librosa.stft((numpy.asfortranarray(wav_voice)), n_fft=n_fft, hop_length=hop_length) stfts_mono.append(stft_mono) stfts_music.append(stft_music) stfts_voice.append(stft_voice) return stfts_mono, stfts_music, stfts_voice #调用wavs_to_specs函数,转化验证集的数据 stfts_mono_valid, stfts_music_valid, stfts_voice_valid = wavs_to_specs(wavs_mono=wavs_mono_valid, wavs_music=wavs_music_valid, wavs_voice=wavs_voice_valid, n_fft=n_fft,hop_length=hop_length) #定义batch_size大小 batch_size = 64 #定义n_fft大小,STFT的窗口大小 n_fft = 1024 #定义存储要转化训练集数据的数组 wavs_mono_train_cut = list() wavs_music_train_cut = list() wavs_voice_train_cut = list() #从训练集中随机选取64个音频数据 for seed in range(batch_size): index = np.random.randint(0,len(wavs_mono_train)) wavs_mono_train_cut.append(wavs_mono_train[index]) wavs_music_train_cut.append(wavs_music_train[index]) wavs_voice_train_cut.append(wavs_voice_train[index]) #短时傅里叶变换,将选取的音频数据转到频域 stfts_mono_train_cut, stfts_music_train_cut, stfts_voice_train_cut = wavs_to_specs(wavs_mono = wavs_mono_train_cut, wavs_music = wavs_music_train_cut, wavs_voice = wavs_voice_train_cut,n_fft = n_fft, hop_length = hop_length) 4. 处理频域文件 频域文件中的数据是复数,包含频率信息和相位信息,但是在训练时只需要考虑频率信息,所以将频率和相位信息分开,使用mini_batch的方法进行训练数据的输入。 #获取频率 def separate_magnitude_phase(data): return np.abs(data), numpy.angle(data) #mini_batch进行数据的输入 #stfts_mono:单声道STFT频域数据 #stfts_music:纯伴奏STFT频域数据 #stfts_music:纯人声STFT频域数据 #batch_size:batch的大小 #sample_frames:获取多少帧数据 def get_next_batch(stfts_mono, stfts_music, stfts_voice, batch_size = 64, sample_frames = 8): stft_mono_batch = list() stft_music_batch = list() stft_voice_batch = list() #随机选择batch_size个数据 collection_size = len(stfts_mono) collection_idx = numpy.random.choice(collection_size, batch_size, replace = True) for idx in collection_idx: stft_mono = stfts_mono[idx] stft_music = stfts_music[idx] stft_voice = stfts_voice[idx] #统计有多少帧 num_frames = stft_mono.shape[1] assert num_frames >= sample_frames #随机获取sample_frames帧数据 start = numpy.random.randint(num_frames - sample_frames + 1) end = start + sample_frames stft_mono_batch.append(stft_mono[:,start:end]) stft_music_batch.append(stft_music[:,start:end]) stft_voice_batch.append(stft_voice[:,start:end]) #将数据转成numpy.array,再对形状做一些变换 #Shape: [batch_size, n_frequencies, n_frames] stft_mono_batch = numpy.array(stft_mono_batch) stft_music_batch = numpy.rray(stft_music_batch) stft_voice_batch = numpy.array(stft_voice_batch) #送入RNN的形状要求: [batch_size, n_frames, n_frequencies] data_mono_batch = stft_mono_batch.transpose((0, 2, 1)) data_music_batch = stft_music_batch.transpose((0, 2, 1)) data_voice_batch = stft_voice_batch.transpose((0, 2, 1)) return data_mono_batch, data_music_batch, data_voice_batch #调用get_next_batch() data_mono_batch, data_music_batch, data_voice_batch = get_next_batch( stfts_mono = stfts_mono_train_cut, stfts_music = stfts_music_train_cut, stfts_voice = stfts_voice_train_cut,batch_size = batch_size, sample_frames = sample_frames) #获取频率值 x_mixed_src, _ = separate_magnitude_phase(data=data_mono_batch) y_music_src, _ = separate_magnitude_phase(data=data_music_batch) y_voice_src, _ = separate_magnitude_phase(data=data_voice_batch) 5. 设定相关路径与参数 #可以通过命令设置的参数 #dataset_dir:数据集路径 #model_dir:模型保存的文件夹 #model_filename:模型保存的文件名 #dataset_sr:数据集音频文件的采样率 #learning_rate:学习率 #batch_size:小批量训练数据的长度 #sample_frames:每次训练获取多少帧数据 #iterations:训练迭代次数 #dropout_rate:丢弃率 def parse_arguments(argv): parser = argparse.ArgumentParser() parser.add_argument('--dataset_train_dir', type=str, help='数据集训练数据路径', default='./dataset/MIR-1K/Wavfile') parser.add_argument('--dataset_validate_dir', type=str, help='数据集验证数据路径', default='./dataset/MIR-1K/UndividedWavfile') parser.add_argument('--model_dir', type=str, help='模型保存的文件夹', default='model') parser.add_argument('--model_filename', type=str, help='模型保存的文件名', default='svmrnn.ckpt') parser.add_argument('--dataset_sr', type=int, help='数据集音频文件的采样率', default=16000) parser.add_argument('--learning_rate', type=float, help='学习率', default=0.0001) parser.add_argument('--batch_size', type=int, help='小批量训练数据的长度', default=64) parser.add_argument('--sample_frames', type=int, help='每次训练获取多少帧数据', default=10) parser.add_argument('--iterations', type=int, help='训练迭代次数', default=30000) parser.add_argument('--dropout_rate', type=float, help='dropout率', default=0.95) return parser.parse_args(argv) 5.3.3模型构建 将数据加载进模型之后,需要定义结构、优化模型和模型实现。 1. 定义结构 定义的架构为1024层循环神经网络,每层RNN的隐藏神经元个数从1~1024递增。最后是一个输入/输出的全连接层。在每层RNN之后,引入进行丢弃正则化,消除模型的过拟合问题。 模型初始化步骤: 按顺序建立1024层RNN,每层拥有从1~1024递增的RNN神经元个数。原始混合数据通过RNN以及丢弃正则化之后,由ReLU函数激活,并利用全连接层输出音频特征值为513的纯伴奏和纯人声的数据。 为了约束输出y_music_src, y_voice_src的大小,即使输出的纯伴奏数据y_music_src和纯人声数据y_voice_src与输入的混合数据x_mixed_src大小相同,输出需要进行以下变换: ym=dmdm+dν+α×xm yv=dvdm+dv+α×xm ym+yv=dm+dvdm+dv+α×xm≈xm 其中,ym是y_music_src,纯伴奏数据; yv是y_voice_src,纯人声数据; dm是y_dense_music_src,全连接层输出的纯伴奏数据; dv是y_dense_voice_src,全连接层输出的人声数据; xm是x_mixed_src,人声和伴奏的混合数据。 在分母上添加一个足够小的数α,防止分母为0。 #保存传入的参数 self.num_features = num_features self.num_rnn_layer = len(num_hidden_units) self.num_hidden_units = num_hidden_units #设置变量 #训练步数 self.g_step = tf.Variable(0, dtype=tf.int32, name='g_step') #设置占位符 #学习率 self.learning_rate = tf.placeholder(tf.float32, shape=[], name='learning_rate') #混合了伴奏和人声的数据 self.x_mixed_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='x_mixed_src') #伴奏数据 self.y_music_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='y_music_src') #人声数据 self.y_voice_src = tf.placeholder(tf.float32, shape=[None, None, num_features], name='y_voice_src') #保持丢弃,用于RNN网络 self.dropout_rate = tf.placeholder(tf.float32) #初始化神经网络 self.y_pred_music_src, self.y_pred_voice_src = self.network_init( #创建会话 self.sess = tf.Session() #构建神经网络 def network_init(self): rnn_layer = [] #根据num_hidden_units的长度来决定创建几层RNN,每个RNN长度为size for size in self.num_hidden_units: #使用LSTM保证大数据集情况下的模型准确度 #加上丢弃,防止过拟合 layer_cell = tf.nn.rnn_cell.LSTMCell(size) layer_cell = tf.contrib.rnn.DropoutWrapper(layer_cell, input_keep_prob=self.dropout_rate) rnn_layer.append(layer_cell) #创建多层RNN #为保证训练时考虑音频的前后时间关系,使用双向RNN multi_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layer) outputs, state = tf.nn.bidirectional_dynamic_rnn(cell_fw = multi_rnn_cell, cell_bw = multi_rnn_cell, inputs = self.x_mixed_src, dtype = tf.float32) out = tf.concat(outputs, 2) #全连接层 #采用ReLU激活 y_dense_music_src = tf.layers.dense( inputs = out, units = self.num_features, activation = tf.nn.relu, name = 'y_dense_music_src') y_dense_voice_src = tf.layers.dense( inputs = out, units = self.num_features, activation = tf.nn.relu, name = 'y_dense_voice_src') y_music_src = y_dense_music_src / (y_dense_music_src + y_dense_voice_src + np.finfo(float).eps) * self.x_mixed_src y_voice_src = y_dense_voice_src / (y_dense_music_src + y_dense_voice_src + np.finfo(float).eps) * self.x_mixed_src return y_music_src, y_voice_src 2. 优化模型 本项目使用的损失函数基于reduce_mean(),计算输出的纯伴奏数据y_music_src及纯人声数据y_voice_src的方差。模型优化器选取Adam优化器,优化模型参数。 #损失函数 def loss_init(self): with tf.variable_scope('loss') as scope: #求方差(reduce_mean方法) loss = tf.reduce_mean( tf.square(self.y_music_src - self.y_pred_music_src) + tf.square(self.y_voice_src - self.y_pred_voice_src), name='loss') return loss #优化器 #采取常用的Adam优化器 def optimizer_init(self): optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(self.loss) return optimizer 3. 模型实现 构建好模型结构、损失函数及优化器之后,定义训练、测试及保存的相关函数,便于使用。 #保存模型 def save(self, directory, filename, global_step): #如果目录不存在,则创建 if not os.path.exists(directory): os.makedirs(directory) self.saver.save(self.sess, os.path.join(directory, filename), global_step=global_step) return os.path.join(directory, filename) #加载模型,如果没有,则初始化所有变量 def load(self, file_dir): #初始化变量 self.sess.run(tf.global_variables_initializer()) #如果没有模型,重新初始化 kpt = tf.train.latest_checkpoint(file_dir) print("kpt:", kpt) startepo = 0 if kpt != None: self.saver.restore(self.sess, kpt) ind = kpt.find("-") startepo = int(kpt[ind + 1:]) return startepo #开始训练 def train(self, x_mixed_src, y_music_src, y_voice_src, learning_rate, dropout_rate): #已经训练的步数 #step = self.sess.run(self.g_step) _, train_loss = self.sess.run([self.optimizer, self.loss], feed_dict = {self.x_mixed_src: x_mixed_src, self.y_music_src: y_music_src, self.y_voice_src: y_voice_src, self.learning_rate:learning_rate,self.dropout_rate: dropout_rate}) return train_loss #验证 def validate(self, x_mixed_src,y_music_src,y_voice_src, dropout_rate): y_music_src_pred, y_voice_src_pred, validate_loss = self.sess.run([self.y_pred_music_src, self.y_pred_voice_src, self.loss], feed_dict = {self.x_mixed_src: x_mixed_src, self.y_music_src: y_music_src, self.y_voice_src: y_voice_src, self.dropout_rate: dropout_rate}) return y_music_src_pred, y_voice_src_pred, validate_loss #测试 def test(self, x_mixed_src, dropout_rate): y_music_src_pred, y_voice_src_pred = self.sess.run([self.y_pred_music_src, self.y_pred_voice_src], feed_dict = {self.x_mixed_src: x_mixed_src, self.dropout_rate: dropout_rate}) return y_music_src_pred, y_voice_src_pred 5.3.4模型训练及保存 本项目使用训练集和测试集拟合,具体包括模型训练及模型保存。 1. 模型训练 模型训练具体操作如下: #初始化模型 model = SVMRNN(num_features = n_fft // 2 + 1, num_hidden_units = num_hidden_units) #加载模型 #如果没有模型,则初始化所有变量 startepo = model.load(file_dir = model_dir) print('startepo:' + str(startepo)) #开始训练 #index是切割训练集位置的标识符 index = 0 for i in (range(iterations)): #从模型中断处开始训练 if i < startepo: continue wavs_mono_train_cut = list() wavs_music_train_cut = list() wavs_voice_train_cut = list() #从训练集中随机选取64个音频数据 for seed in range(batch_size): index = np.random.randint(0,len(wavs_mono_train)) wavs_mono_train_cut.append(wavs_mono_train[index]) wavs_music_train_cut.append(wavs_music_train[index]) wavs_voice_train_cut.append(wavs_voice_train[index]) #短时傅里叶变换,将选取的音频数据转到频域 stfts_mono_train_cut, stfts_music_train_cut, stfts_voice_train_cut = wavs_to_specs( wavs_mono = wavs_mono_train_cut, wavs_music = wavs_music_train_cut, wavs_voice = wavs_voice_train_cut, n_fft = n_fft, hop_length = hop_length) #获取下一批数据 data_mono_batch, data_music_batch, data_voice_batch = get_next_batch( stfts_mono = stfts_mono_train_cut, stfts_music = stfts_music_train_cut, stfts_voice = stfts_voice_train_cut, batch_size = batch_size, sample_frames = sample_frames) #获取频率值 x_mixed_src, _ = separate_magnitude_phase(data = data_mono_batch) y_music_src, _ = separate_magnitude_phase(data = data_music_batch) y_voice_src, _ = separate_magnitude_phase(data = data_voice_batch) #送入神经网络,开始训练 train_loss = model.train(x_mixed_src = x_mixed_src, y_music_src = y_music_src, y_voice_src = y_voice_src,learning_rate = learning_rate, dropout_rate = dropout_rate) #每10步输出一次训练结果的损失值 if i % 10 == 0: print('Step: %d Train Loss: %f' %(i, train_loss)) #每200步输出一次测试结果 if i % 200 == 0: print('==============================================') data_mono_batch, data_music_batch, data_voice_batch = get_next_batch(stfts_mono = stfts_mono_valid, stfts_music = stfts_music_valid,stfts_voice = stfts_voice_valid, batch_size=batch_size, sample_frames = sample_frames) x_mixed_src, _ = separate_magnitude_phase(data = data_mono_batch) y_music_src, _ =separate_magnitude_phase(data = data_music_batch) y_voice_src, _ =separate_magnitude_phase(data = data_voice_batch) y_music_src_pred, y_voice_src_pred, validate_loss = model.validate(x_mixed_src = x_mixed_src, y_music_src = y_music_src, y_voice_src = y_voice_src, dropout_rate = dropout_rate) print('Step: %d Validation Loss: %f' %(i, validate_loss)) print('==============================================') batch_size是在一次前向/后向传播过程用到的训练样例数量,训练时随机选取64个数据并开始训练,总共训练1110个数据,迭代30 000步,如图55所示。 图55训练结果 2. 模型保存 模型保存有两种作用: 一是为了在训练过程中出现意外而中断时,能够在上次保存的模型处开始; 二是为了在应用中直接使用训练好的模型。 #每200步保存一次模型 if i % 200 == 0: model.save(directory = model_dir, filename = model_filename, global_step=i) 5.3.5模型测试 采用Python自带的Tkinter库进行GUI设计,GUI实现如下功能。 1. 批量选取歌曲和保存路径 定义addfile()和choose_save_path()两个函数,使用Tkinter中filedialog模块打开系统路径。 def addfile(): #定义全局变量music_path,用于添加音频文件 global music_path paths = tk.filedialog.askopenfilenames(title='选择要分离的歌曲') #保存选择的歌曲 #遍历添加 for path in paths: music_path.append(path) label_info['text'] = '\n'.join(music_path) #选择分离结束后保存的文件夹 def choose_save_path(): global save_path save_path = tk.filedialog.askdirectory(title='选择保存文件夹') save_info['text'] = save_path 2. 模型导入及调用 定义调用模型的函数separate()。该函数完成如下功能: (1) 将带转化的文件进行列表保存、数据分割、短时傅里叶变换,完成数据的预处理。 #加载音频文件 wavs_mono = list() for filename in music_path: wav_mono, _ = librosa.load(filename, sr=dataset_sr, mono=True) wavs_mono.append(wav_mono) #短时傅里叶变换的fft点数 #默认情况下,窗口长度 = fft点数 n_fft = 1024 #冗余度 hop_length = n_fft // 4 #将音频数据转换到频域 stfts_mono = list() for wav_mono in wavs_mono: stft_mono=librosa.stft(wav_mono,n_fft=n_fft,hop_length=hop_length) stfts_mono.append(stft_mono.transpose()) (2) 数据预处理后,调用训练好的模型,进行人声和伴奏的分离。 #初始化神经网络 model = SVMRNN(num_features=n_fft // 2 + 1, num_hidden_units=num_hidden_units) #导入模型 model.load(file_dir=model_dir) for wav_filename, wav_mono, stft_mono in zip(music_path, wavs_mono, stfts_mono): wav_filename_base = os.path.basename(wav_filename) #单声道音频文件 wav_mono_filename = wav_filename_base.split('.')[0] + '_mono.wav' #分离后的纯伴奏音频文件 wav_music_filename = wav_filename_base.split('.')[0] + '_music.wav' #分离后的纯人声音频文件 wav_voice_filename = wav_filename_base.split('.')[0] + '_voice.wav' #要保存文件的相对路径 wav_mono_filepath = os.path.join(save_path, wav_mono_filename) wav_music_hat_filepath=os.path.join(save_path,wav_music_filename) wav_voice_hat_filepath=os.path.join(save_path,wav_voice_filename) print('Processing %s ...' % wav_filename_base) stft_mono_magnitude, stft_mono_phase = separate_magnitude_phase(data=stft_mono) stft_mono_magnitude = np.array([stft_mono_magnitude]) y_music_pred, y_voice_pred = model.test(x_mixed_src=stft_mono_magnitude, dropout_rate=dropout_rate) (3) 将处理好的频率文件和原本的相位信息相加,进行傅里叶逆变换。 #根据振幅和相位,得到复数 #信号s(t)乘上e^(j*phases)表示信号s(t)移动相位phases def combine_magnitude_phase(magnitudes, phases): return magnitudes * np.exp(1.j * phases) (4) 将时域文件写成.wav格式的歌曲保存。 #保存数据,使用librosa.output.write_wav()函数,将文件保存成.wav格式歌曲文件 librosa.output.write_wav(wav_mono_filepath, wav_mono, dataset_sr) librosa.output.write_wav(wav_music_hat_filepath,y_music_hat,dataset_sr) librosa.output.write_wav(wav_voice_hat_filepath,y_voice_hat,dataset_sr) #检测在保存文件夹中是否生成了伴奏文件,若存在则自动打开该文件夹 if os.path.exists(wav_music_hat_filepath): os.startfile(save_path) remind_window.destroy() 3. GUI代码 GUI相关代码如下: from tkinter import Tk, filedialog import tkinter as tk import librosa import os import numpy as np from NewModel import SVMRNN from NewUtils import separate_magnitude_phase, combine_magnitude_phase music_path = [] save_path = str() wav_music_hat_filepath = str() def addfile(): #定义全局变量music_path,用于添加音频文件 global music_path paths = tk.filedialog.askopenfilenames(title='选择要分离的歌曲') #保存选择的歌曲 #遍历添加 for path in paths: music_path.append(path) label_info['text'] = '\n'.join(music_path) #选择分离结束后保存的文件夹 def choose_save_path(): global save_path save_path = tk.filedialog.askdirectory(title='选择保存文件夹') save_info['text'] = save_path #弹窗信息的定义 def pop_window(): global wav_music_hat_filepath def separate(): dataset_sr = 16000 #采样率 model_dir = './model' #模型保存文件夹 dropout_rate = 0.95 #丢弃率 #加载音频文件 wavs_mono = list() for filename in music_path: wav_mono, _ = librosa.load(filename, sr=dataset_sr, mono=True) wavs_mono.append(wav_mono) #短时傅里叶变换的fft点数 #默认情况下,窗口长度 = fft点数 n_fft = 1024 #冗余度 hop_length = n_fft // 4 #用于创建RNN节点数 num_hidden_units = [1024, 1024, 1024, 1024, 1024] #将音频数据转换到频域 stfts_mono = list() for wav_mono in wavs_mono: stft_mono=librosa.stft(wav_mono,n_fft=n_fft,hop_length=hop_length) stfts_mono.append(stft_mono.transpose()) #初始化神经网络 model=SVMRNN(num_features=n_fft//2+1,num_hidden_units=num_hidden_units) #导入模型 model.load(file_dir=model_dir) for wav_filename, wav_mono, stft_mono in zip(music_path, wavs_mono, stfts_mono): wav_filename_base = os.path.basename(wav_filename) #单声道音频文件 wav_mono_filename = wav_filename_base.split('.')[0] + '_mono.wav' #分离后的纯伴奏音频文件 wav_music_filename=wav_filename_base.split('.')[0]+'_music.wav' #分离后的纯人声音频文件 wav_voice_filename=wav_filename_base.split('.')[0] + '_voice.wav' #要保存文件的相对路径 wav_mono_filepath = os.path.join(save_path, wav_mono_filename) wav_music_hat_filepath=os.path.join(save_path,wav_music_filename) wav_voice_hat_filepath=os.path.join(save_path,wav_voice_filename) print('Processing %s ...' % wav_filename_base) stft_mono_magnitude, stft_mono_phase = separate_magnitude_phase(data=stft_mono) stft_mono_magnitude = np.array([stft_mono_magnitude]) y_music_pred, y_voice_pred = model.test(x_mixed_src=stft_mono_magnitude, dropout_rate=dropout_rate) #根据振幅和相位,转为复数,用于下面的逆短时傅里叶变换 y_music_stft_hat = combine_magnitude_phase(magnitudes=y_music_pred[0], phases=stft_mono_phase) y_voice_stft_hat = combine_magnitude_phase(magnitudes=y_voice_pred[0], phases=stft_mono_phase) y_music_stft_hat = y_music_stft_hat.transpose() y_voice_stft_hat = y_voice_stft_hat.transpose() #通过逆短时傅里叶变换,将分离好的频域数据转换为音频,生成相对应的音频文件 y_music_hat=librosa.istft(y_music_stft_hat, hop_length=hop_length) y_voice_hat=librosa.istft(y_voice_stft_hat, hop_length=hop_length) #保存数据 librosa.output.write_wav(wav_mono_filepath,wav_mono,dataset_sr) librosa.output.write_wav(wav_music_hat_filepath,y_music_hat, dataset_sr) librosa.output.write_wav(wav_voice_hat_filepath,y_voice_hat, dataset_sr) if os.path.exists(wav_music_hat_filepath): os.startfile(save_path) remind_window.destroy() remind_window = tk.Toplevel() remind_window.title('提示') remind_window.minsize(width=400, height=200) tk.Label(remind_window, text='加载模型中,请勿关闭软件').place(x=70, y=60) tk.Button(remind_window, text='我知道了', font=('Fangsong', 14), command=separate).place(x=150, y=100) root = Tk() #窗口标题 root.title('歌曲人声分离') #大小不可调整 root.resizable(0,0) #创建背景图片 canvas=tk.Canvas(root, width=800, height=900, bd=0, highlightthickness=0) imgpath = 'image/bg1.jpg' img = Image.open(imgpath) photo = ImageTk.PhotoImage(img) #设置背景图片在窗口显示的偏移量 canvas.create_image(750, 400, image=photo) canvas.pack() #添加按钮 #添加按钮的图片 btn_add = tk.PhotoImage(file='image/btn_add.png') btn_addfile = tk.Button(root, command=addfile, image=btn_add) #将按钮摆放到窗口上 canvas.create_window(70,70, width=84, height=84, window=btn_addfile) #功能说明文字 canvas.create_text(70,130, text='添加文件', fill='white', font=('Fangsong', '15', 'bold')) #选择保存路径的按钮 pic_save = tk.PhotoImage(file='image/btn_save.png') btn_save = tk.Button(root, command=choose_save_path, image=pic_save) canvas.create_window(200, 70, width=84, height=84, window=btn_save) canvas.create_text(200,130,text='选择保存路径',fill='white', font=('Fangsong', '15', 'bold')) #运行按钮 pic_run = tk.PhotoImage(file='image/btn_run.png') btn_run = tk.Button(root, command=pop_window, image=pic_run) canvas.create_window(330, 70, width=84, height=84, window=btn_run) canvas.create_text(330, 130, text='运行', fill='white', font=('Fangsong', '15', 'bold')) #显示待分离歌曲 canvas.create_text(70, 180, text='待分离的歌曲', fill='white', font=('Fangsong', '14', 'bold')) label_info = tk.Label(root, bg='white', anchor='nw', justify='left') canvas.create_window(210, 300, width=400, height=200, window=label_info) #显示保存的路径 canvas.create_text(48, 430, text='保存路径', fill='white', font=('Fangsong', '14', 'bold')) save_info = tk.Label(root, bg='white', anchor='nw', justify='left') canvas.create_window(210, 470, width=400, height=50, window=save_info) root.mainloop() 5.4系统测试 本部分包括训练准确率、测试效果及模型应用。 5.4.1训练准确率 训练迭代到靠后的步数,损失函数的值小于0.5,这意味着这个预测模型训练比较成功。在整个迭代训练过程中,随着epoch的增加,模型损失函数的值在逐渐减小,并且在17 000步以后趋于稳定,如图56所示。 图56训练准确率 5.4.2测试效果 将数据代入模型进行测试,并将分离得到的纯伴奏和纯人声波形经过对比以及人耳辨别,得到验证: 模型可以实现歌曲伴奏和人声分离。如图57和图58所示。 图57分离的纯人声波形 图58分离的纯伴奏波形 5.4.3模型应用 使用说明包括以下4部分。 (1) 打开gui.py文件,界面如图59所示。 (2) 单击“添加文件”按钮,选择要获得伴奏的歌曲,在“待分离的歌曲”中显示添加歌曲的路径和名称,如图510所示。 图59主界面 图510添加歌曲 (3) 选择保存路径,如图511所示。 图511保存路径 (4) 单击“运行”按钮后会弹出提示框,确认后程序开始运行,运行完毕后将自动打开保存文件夹,如图512所示。 图512运行结果