MATLAB环境下基于深度学习的语音降噪方法

这篇具有很好参考价值的文章主要介绍了MATLAB环境下基于深度学习的语音降噪方法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

之前简单的利用深层自编码器对语音信号进行降噪

基于自编码器的语音信号降噪 - 哥廷根数学学派的文章 - 知乎 基于自编码器的语音信号降噪 - 知乎

本篇讲一些稍微复杂的基于深度学习的语音降噪方法,并比较了应用于同一任务的两种的网络:全连接层网络和卷积网络。

完整代码和数据集见如下链接

🍞正在为您运送作品详情

考虑以下以 8 kHz 采样的语音信号

[cleanAudio,fs] = audioread("SpeechDFT.wav");
sound(cleanAudio,fs)

将洗衣机噪声添加到上述的语音信号中,设置噪声功率,使信噪比 (SNR) 为0dB

noise = audioread("WashingMachine.mp3");

接下来从噪声文件中的随机位置提取噪声段

ind = randi(numel(noise) - numel(cleanAudio) + 1, 1, 1);
noiseSegment = noise(ind:ind + numel(cleanAudio) - 1);
speechPower = sum(cleanAudio.^2);
noisePower = sum(noiseSegment.^2);
noisyAudio = cleanAudio + sqrt(speechPower/noisePower) * noiseSegment;
%播放信号
sound(noisyAudio,fs)

可视化原始信号和噪声信号

t = (1/fs) * (0:numel(cleanAudio)-1);
subplot(2,1,1)
plot(t,cleanAudio)
title("Clean Audio")
grid on
subplot(2,1,2)
plot(t,noisyAudio)
title("Noisy Audio")
xlabel("Time (s)")
grid on

MATLAB环境下基于深度学习的语音降噪方法

语音降噪的目的是从语音信号中去除洗衣机噪声,同时最大限度地减少输出语音信号中不希望的所谓的artifacts。

检查数据集

本例使用的数据集包含 48 kHz 的短句录音

MATLAB环境下基于深度学习的语音降噪方法

训练集,测试集,验证集文件

MATLAB环境下基于深度学习的语音降噪方法

训练集部分数据

使用 audioDatastore 为训练集创建数据存储

adsTrain = audioDatastore(fullfile(dataFolder,'train'),'IncludeSubfolders',true);

读取datastore中第一个文件的内容

[audio,adsTrainInfo] = read(adsTrain);

播放语音信号

sound(audio,adsTrainInfo.SampleRate)

绘制语音信号

figure
t = (1/adsTrainInfo.SampleRate) * (0:numel(audio)-1);
plot(t,audio)
title("Example Speech Signal")
xlabel("Time (s)")
grid on

MATLAB环境下基于深度学习的语音降噪方法

深度学习系统概述

基本的深度学习训练方案如下图所示。

MATLAB环境下基于深度学习的语音降噪方法

对于熟悉语音信号处理的同学肯定是小case。注意,由于语音通常低于 4 kHz,因此首先将干净和嘈杂的语音信号下采样到 8 kHz,以减少网络的计算负担。 网络的输出是降噪信号的幅度谱,使用输出幅度谱和噪声信号的相位将降噪后的音频转换回时域[1]。

可以使用短时傅里叶变换 (STFT) 将音频信号转换到频域,使用Hamming窗,窗口长度为 256 ,重叠率为 75%。Predicter的输入由 8 个连续的噪声 STFT 向量组成,因此每个 STFT 输出估计值都是基于当前的噪声 STFT 和 7 个先前的噪声 STFT 向量计算的。

MATLAB环境下基于深度学习的语音降噪方法

如何从一个训练文件生成Target和Predicter(直接用英文单词了,否则容易引起误解)?首先,定义系统参数:

windowLength = 256;
win = hamming(windowLength,"periodic");
overlap = round(0.75 * windowLength);
ffTLength = windowLength;
inputFs = 48e3;
fs = 8e3;
numFeatures = ffTLength/2 + 1;
numSegments = 8;

创建一个 dsp.SampleRateConverter 对象以将 48 kHz 音频转换为 8 kHz,

src = dsp.SampleRateConverter("InputSampleRate",inputFs, ...
                              "OutputSampleRate",fs, ...
                              "Bandwidth",7920);

从datastore中读入音频文件的内容

audio = read(adsTrain);

注意:要确保音频长度是采样率转换器抽取因子的倍数

decimationFactor = inputFs/fs;
L = floor(numel(audio)/decimationFactor);
audio = audio(1:decimationFactor*L);

将音频信号转换为 8 kHz

audio = src(audio);
reset(src)

从洗衣机噪声向量中创建一个随机噪声段

randind = randi(numel(noise) - numel(audio),[1 1]);
noiseSegment = noise(randind : randind + numel(audio) - 1);

向语音信号中添加噪声,使 SNR 为 0 dB。

noisePower = sum(noiseSegment.^2);
cleanPower = sum(audio.^2);
noiseSegment = noiseSegment .* sqrt(cleanPower/noisePower);
noisyAudio = audio + noiseSegment;

使用STFT从原始和嘈杂的音频信号生成幅值STFT向量

cleanSTFT = stft(audio,'Window',win,'OverlapLength',overlap,'FFTLength',ffTLength);
cleanSTFT = abs(cleanSTFT(numFeatures-1:end,:));
noisySTFT = stft(noisyAudio,'Window',win,'OverlapLength',overlap,'FFTLength',ffTLength);
noisySTFT = abs(noisySTFT(numFeatures-1:end,:));

从嘈杂的 STFT 生成 8 段训练Predicter信号,对应深度学习训练方案图

noisySTFT = [noisySTFT(:,1:numSegments - 1), noisySTFT];
stftSegments = zeros(numFeatures, numSegments , size(noisySTFT,2) - numSegments + 1);
for index = 1:size(noisySTFT,2) - numSegments + 1
    stftSegments(:,:,index) = (noisySTFT(:,index:index + numSegments - 1)); 
end

设置Target和Predicter,每个Predicter维度为 129×8,每个Target为 129×1,这些都是可以根据信号调整的

targets = cleanSTFT;
size(targets)
predictors = stftSegments;
size(predictors)

为了加快处理速度,使用 tall 数组从datastore中所有音频文件的语音片段中提取特征序列,要是有matlab的并行工具箱和GPU。

首先,将datastore转换为 tall 数组,此处需要的时间较长

reset(adsTrain)
T = tall(adsTrain)

从 tall 表中提取Target和Predicter的幅值STFT,这一步很重要,GenerateSpeechDenoisingFeatures是提取Target和Predicter的幅值STFT的函数,我还需要优化一点

[targets,predictors] = cellfun(@(x)GenerateSpeechDenoisingFeatures(x,noise,src),T,"UniformOutput",false);

[targets,predictors] = gather(targets,predictors);

将所有特征进行归一化,分别计算Target和Predicter的均值和标准差,并使用它们对数据进行归一化。

predictors = cat(3,predictors{:});
noisyMean = mean(predictors(:));
noisyStd = std(predictors(:));
predictors(:) = (predictors(:) - noisyMean)/noisyStd;
targets = cat(2,targets{:});
cleanMean = mean(targets(:));
cleanStd = std(targets(:));
targets(:) = (targets(:) - cleanMean)/cleanStd;

将Target和Predicter进行维度重塑,即reshape为与深度学习网络相对应的维度

predictors = reshape(predictors,size(predictors,1),size(predictors,2),1,size(predictors,3));
targets = reshape(targets,1,1,size(targets,1),size(targets,2));

将数据随机分成训练集和验证集。

inds = randperm(size(predictors,4));
L = round(0.99 * size(predictors,4));

trainPredictors = predictors(:,:,:,inds(1:L));
trainTargets = targets(:,:,:,inds(1:L));

validatePredictors = predictors(:,:,:,inds(L+1:end));
validateTargets = targets(:,:,:,inds(L+1:end));

下面开始步入正题,进行第一个全连接层深层网络的语音降噪

全连接网络是什么就不讲了,看图

MATLAB环境下基于深度学习的语音降噪方法

定义网络层,将输入大小指定为大小为 NumFeatures-by-NumSegments(本例中为 129-by-8)的图像。 定义两个隐藏的全连接层,每个层有 1024 个神经元。 由于是纯线性系统,因此在每个隐藏的全连接层之后都有一个整流线性单元 (ReLU) 层。 批量归一化层对输出的均值和标准差进行归一化,添加一个具有 129 个神经元的全连接层,然后是一个回归层。

layers = [
    imageInputLayer([numFeatures,numSegments])
    fullyConnectedLayer(1024)
    batchNormalizationLayer
    reluLayer
    fullyConnectedLayer(1024)
    batchNormalizationLayer
    reluLayer
    fullyConnectedLayer(numFeatures)
    regressionLayer
    ];

然后设置网络的训练选项,很好理解

miniBatchSize = 128;
options = trainingOptions("adam", ...
    "MaxEpochs",3, ...
    "InitialLearnRate",1e-5,...
    "MiniBatchSize",miniBatchSize, ...
    "Shuffle","every-epoch", ...
    "Plots","training-progress", ...
    "Verbose",false, ...
    "ValidationFrequency",floor(size(trainPredictors,4)/miniBatchSize), ...
    "LearnRateSchedule","piecewise", ...
    "LearnRateDropFactor",0.9, ...
    "LearnRateDropPeriod",1, ...
    "ValidationData",{validatePredictors,validateTargets});

利用trainNetwork 使用指定的训练选项和网络层训练深层网络。 因为训练集很大,训练过程较为耗时

    denoiseNetFullyConnected = trainNetwork(trainPredictors,trainTargets,layers,options);

计算网络全连接层的权重数量

numWeights = 0;
for index = 1:numel(denoiseNetFullyConnected.Layers)
    if isa(denoiseNetFullyConnected.Layers(index),"nnet.cnn.layer.FullyConnectedLayer")
        numWeights = numWeights + numel(denoiseNetFullyConnected.Layers(index).Weights);
    end
end
fprintf("The number of weights is %d.\n",numWeights);

然后进行卷积层神经网络的语音去噪

卷积层通常比全连接层包含更少的参数,根据文献[2]中描述的全卷积网络的层数,包括 16 个卷积层。 前 15 个卷积层为3层的组,重复 5 次,滤波器宽度分别为 9、5 和 9,滤波器数量分别为 18、30 和 8。 最后一个卷积层的滤波器宽度为129 。 在这个网络中,卷积只在一个方向(沿着频率维度)执行,并且除了第一层之外的所有层,沿着时间维度的滤波器宽度设置为 1。 与全连接网络类似,卷积层之后是 ReLu 和批量归一化层。

layers = [imageInputLayer([numFeatures,numSegments])
          convolution2dLayer([9 8],18,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer
          
          repmat( ...
          [convolution2dLayer([5 1],30,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer
          convolution2dLayer([9 1],8,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer
          convolution2dLayer([9 1],18,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer],4,1)
          
          convolution2dLayer([5 1],30,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer
          convolution2dLayer([9 1],8,"Stride",[1 100],"Padding","same")
          batchNormalizationLayer
          reluLayer
          
          convolution2dLayer([129 1],1,"Stride",[1 100],"Padding","same")
          
          regressionLayer
          ];

训练选项与全连接网络的选项类似

options = trainingOptions("adam", ...
    "MaxEpochs",3, ...
    "InitialLearnRate",1e-5, ...
    "MiniBatchSize",miniBatchSize, ...
    "Shuffle","every-epoch", ...
    "Plots","training-progress", ...
    "Verbose",false, ...
    "ValidationFrequency",floor(size(trainPredictors,4)/miniBatchSize), ...
    "LearnRateSchedule","piecewise", ...
    "LearnRateDropFactor",0.9, ...
    "LearnRateDropPeriod",1, ...
    "ValidationData",{validatePredictors,permute(validateTargets,[3 1 2 4])});

利用 trainNetwork 使用指定的训练选项和层架构训练网络

    denoiseNetFullyConvolutional = trainNetwork(trainPredictors,permute(trainTargets,[3 1 2 4]),layers,options);

计算网络全连接层的权重数量

numWeights = 0;
for index = 1:numel(denoiseNetFullyConvolutional.Layers)
    if isa(denoiseNetFullyConvolutional.Layers(index),"nnet.cnn.layer.Convolution2DLayer")
        numWeights = numWeights + numel(denoiseNetFullyConvolutional.Layers(index).Weights);
    end
end
fprintf("The number of weights in convolutional layers is %d\n",numWeights);

测试降噪网络

读入测试集

adsTest = audioDatastore(fullfile(dataFolder,'test'),'IncludeSubfolders',true);

从datastore中读取文件

[cleanAudio,adsTestInfo] = read(adsTest);

确保音频长度是采样率转换器抽取因子的倍数

L = floor(numel(cleanAudio)/decimationFactor);
cleanAudio = cleanAudio(1:decimationFactor*L);

将音频信号转换为 8 kHz。

cleanAudio = src(cleanAudio);
reset(src)

测试阶段使用训练阶段未使用的洗衣机噪声来给语音信号加噪

noise = audioread("WashingMachine-16-8-mono-200secs.mp3");

从洗衣机噪声向量中创建一个随机噪声段

randind = randi(numel(noise) - numel(cleanAudio), [1 1]);
noiseSegment = noise(randind : randind + numel(cleanAudio) - 1);

向语音信号中添加噪声,使 SNR为0 dB

noisePower = sum(noiseSegment.^2);
cleanPower = sum(cleanAudio.^2);
noiseSegment = noiseSegment .* sqrt(cleanPower/noisePower);
noisyAudio = cleanAudio + noiseSegment;

同样使用STFT从嘈杂的语音信号中生成幅值STFT向量

noisySTFT = stft(noisyAudio,'Window',win,'OverlapLength',overlap,'FFTLength',ffTLength);
noisyPhase = angle(noisySTFT(numFeatures-1:end,:));
noisySTFT = abs(noisySTFT(numFeatures-1:end,:));

同样从STFT 生成 8 段训练Predicter信号

noisySTFT = [noisySTFT(:,1:numSegments-1) noisySTFT];
predictors = zeros( numFeatures, numSegments , size(noisySTFT,2) - numSegments + 1);
for index = 1:(size(noisySTFT,2) - numSegments + 1)
    predictors(:,:,index) = noisySTFT(:,index:index + numSegments - 1); 
end

通过在训练阶段计算的均值和标准差对Predicter进行归一化。

predictors(:) = (predictors(:) - noisyMean) / noisyStd;

通过对两个经过训练的网络计算降噪幅值STFT

predictors = reshape(predictors, [numFeatures,numSegments,1,size(predictors,3)]);
STFTFullyConnected = predict(denoiseNetFullyConnected, predictors);
STFTFullyConvolutional = predict(denoiseNetFullyConvolutional, predictors);

通过训练阶段使用的平均值和标准差来恢复输出

STFTFullyConnected(:) = cleanStd * STFTFullyConnected(:) + cleanMean;
STFTFullyConvolutional(:) = cleanStd * STFTFullyConvolutional(:) + cleanMean;

将单边STFT 转换为“居中的” STFT,这在信号处理中很好理解

STFTFullyConnected = STFTFullyConnected.' .* exp(1j*noisyPhase);
STFTFullyConnected = [conj(STFTFullyConnected(end-1:-1:2,:)); STFTFullyConnected];
STFTFullyConvolutional = squeeze(STFTFullyConvolutional) .* exp(1j*noisyPhase);
STFTFullyConvolutional = [conj(STFTFullyConvolutional(end-1:-1:2,:)) ; STFTFullyConvolutional];

计算降噪语音信号。 istft 执行逆 STFT,使用带噪声STFT相位的相位来重建时域信号

denoisedAudioFullyConnected = istft(STFTFullyConnected,  ...
                                    'Window',win,'OverlapLength',overlap, ...
                                    'FFTLength',ffTLength,'ConjugateSymmetric',true);
                                
denoisedAudioFullyConvolutional = istft(STFTFullyConvolutional,  ...
                                        'Window',win,'OverlapLength',overlap, ...
                                        'FFTLength',ffTLength,'ConjugateSymmetric',true);

绘制干净、嘈杂和降噪后的音频信号

t = (1/fs) * (0:numel(denoisedAudioFullyConnected)-1);

figure

subplot(4,1,1)
plot(t,cleanAudio(1:numel(denoisedAudioFullyConnected)))
title("Clean Speech")
grid on

subplot(4,1,2)
plot(t,noisyAudio(1:numel(denoisedAudioFullyConnected)))
title("Noisy Speech")
grid on

subplot(4,1,3)
plot(t,denoisedAudioFullyConnected)
title("Denoised Speech (Fully Connected Layers)")
grid on

subplot(4,1,4)
plot(t,denoisedAudioFullyConvolutional)
title("Denoised Speech (Convolutional Layers)")
grid on
xlabel("Time (s)")

MATLAB环境下基于深度学习的语音降噪方法

绘制干净、嘈杂和降噪后的频谱图。

h = figure;

subplot(4,1,1)
spectrogram(cleanAudio,win,overlap,ffTLength,fs);
title("Clean Speech")
grid on

subplot(4,1,2)
spectrogram(noisyAudio,win,overlap,ffTLength,fs);
title("Noisy Speech")
grid on

subplot(4,1,3)
spectrogram(denoisedAudioFullyConnected,win,overlap,ffTLength,fs);
title("Denoised Speech (Fully Connected Layers)")
grid on

subplot(4,1,4)
spectrogram(denoisedAudioFullyConvolutional,win,overlap,ffTLength,fs);
title("Denoised Speech (Convolutional Layers)")
grid on

p = get(h,'Position');
set(h,'Position',[p(1) 65 p(3) 800]);

MATLAB环境下基于深度学习的语音降噪方法

MATLAB环境下基于深度学习的语音降噪方法

sound(noisyAudio,fs)

播放全连接层神经网络降噪后的语音信号

sound(denoisedAudioFullyConnected,fs)

播放卷积层神经网络降噪后的语音信号

sound(denoisedAudioFullyConvolutional,fs)

播放干净的语音信号

sound(cleanAudio,fs)

测试更多文件,且生成时域图和频域图,同时返回干净、加噪和降噪后的语音信号

[cleanAudio,noisyAudio,denoisedAudioFullyConnected,denoisedAudioFullyConvolutional] = testDenoisingNets(adsTest,denoiseNetFullyConnected,denoiseNetFullyConvolutional,noisyMean,noisyStd,cleanMean,cleanStd);

MATLAB环境下基于深度学习的语音降噪方法

MATLAB环境下基于深度学习的语音降噪方法

MATLAB环境下基于深度学习的语音降噪方法

训练非常耗时,必须要配备matlab并行工具箱和GPU

另外,此方法可迁移至其他的一维信号,比如微震信号,机械振动信号,心电信号等,但要特别注意所加噪声信号的相位问题。

参考文献

[1] "Experiments on Deep Learning for Speech Denoising", Ding Liu, Paris Smaragdis, Minje Kim, INTERSPEECH, 2014.

[2] "A Fully Convolutional Neural Network for Speech Enhancement", Se Rim Park, Jin Won Lee, INTERSPEECH, 2017.文章来源地址https://www.toymoban.com/news/detail-442693.html

到了这里,关于MATLAB环境下基于深度学习的语音降噪方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于树莓派构建深度学习语音识别系统

    +v hezkz17进数字音频系统研究开发交流答疑裙   1 Linux 音频框架如何做语音识别系统?   要在Linux上构建一个语音识别系统,可以使用以下步骤和工具: 安装音频框架:在Linux上运行语音识别系统需要一个适当的音频框架。常见的选择包括 ALSA(Advanced Linux Sound Architecture)和

    2024年02月15日
    浏览(51)
  • 170基于matlab的DNCNN图像降噪

    基于matlab的DNCNN图像降噪,网络分为三部分,第一部分为Conv+Relu(一层),第二部分为Conv+BN+Relu(若干层),第三部分为Conv(一层),网络层数为17或者20层。网络学习的是图像残差,也就是带噪图像和无噪图像差值,损失函数采用的MSE。程序已调通,可直接运行。 170 matlab

    2024年02月22日
    浏览(33)
  • 启英泰伦通话降噪方案,采用深度学习降噪算法,让通话更清晰

    生活中的通话应用场景无处不在,如电话、对讲机、远程会议、在线教育等。普遍存在的问题是环境噪音、干扰声导致通话声音不清晰,语音失真等。 为了解决这一问题,启英泰伦基于自适应线性滤波联合非线性滤波的回声消除方案和基于深度学习的降噪方案推出了通话降噪

    2024年02月11日
    浏览(35)
  • 基于深度学习的语音识别算法的设计与实现

    收藏和点赞,您的关注是我创作的动力   语音识别(Speech Recognition)是一种让机器通过识别音频把语音信号转变为相 应的文本或命令的技术语音识别技术主要有模式匹配识别法,声学特征提取,声学模型 建模 ,语言模型建模等技术组成。借助机器学习领域中的深度学习的

    2024年02月06日
    浏览(51)
  • 基于深度学习的多模态语音识别与合成

    作者:禅与计算机程序设计艺术 语音识别(ASR)、语音合成(TTS)及其相关技术一直是当今人工智能领域的一大热点,也是当前研究的重点方向之一。近年来随着深度学习技术的不断突破,多模态语音理解和处理技术的进步,结合深度学习方法的多模态语音识别系统得到了广

    2024年02月10日
    浏览(65)
  • 基于深度学习的多模态语音识别:如何提高语音识别准确率和鲁棒性

    作者:禅与计算机程序设计艺术 随着语音识别技术的发展,采用多种模态(声学、语言模型、视觉特征等)进行联合建模,基于深度学习的多模态语音识别取得了新进展。传统的声学模型或手工特征工程方法已经无法满足实时、高精度、低延迟的需求,多模态语音识别需要解决

    2024年02月13日
    浏览(71)
  • 【语音处理】基于加权压力匹配方法(WPMM)的私人声音系统研究(Matlab代码实现)

    💥💥💞💞 欢迎来到本博客 ❤️❤️💥💥 🏆博主优势: 🌞🌞🌞 博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️ 座右铭: 行百里者,半于九十。 📋📋📋 本文目录如下: 🎁🎁🎁 目录 💥1 概述 📚2 运行结果 🌈3 Matlab代码实现 🎉4 参考文献 在本文中

    2024年02月15日
    浏览(48)
  • 基于深度学习的中文语音识别系统(计算机毕设 附完整代码)

    该系统实现了基于深度框架的语音识别中的声学模型和语言模型建模,其中声学模型包括 CNN-CTC、GRU-CTC、CNN-RNN-CTC,语言模型包含 transformer、CBHG,数据集包含 stc、primewords、Aishell、thchs30 四个数据集。 本项目现已训练一个迷你的语音识别系统,将项目下载到本地上,下载 th

    2024年02月11日
    浏览(80)
  • 基于百度语音识别API智能语音识别和字幕推荐系统——深度学习算法应用(含全部工程源码)+测试数据集

    本项目基于百度语音识别API,结合了语音识别、视频转换音频识别以及语句停顿分割识别等多种技术,从而实现了高效的视频字幕生成。 首先,我们采用百度语音识别API,通过对语音内容进行分析,将音频转换成文本。这个步骤使得我们能够从语音中提取出有意义的文本信息

    2024年02月13日
    浏览(57)
  • 基于Python+百度语音的智能语音ChatGPT聊天机器人(机器学习+深度学习+语义识别)含全部工程源码 适合个人二次开发

    本项目基于机器学习和语义识别技术,让机器人理解文本并进行合适的答复。伙伴们可以通过该工程源码,进行个人二次开发,比如使用语音与机器人交流,实现智能问答、智能音箱及智能机器宠物等等。 当然针对现在最火爆的 ChatGPT等通用大语言模型 ,伙伴们可以直接将其

    2024年02月07日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包