pytorch深度学习入门(5)之-Torchaudio音频加载

Torchaudio简介

Torchaudio是一个用于处理音频数据的Python库,它是基于PyTorch的扩展库,提供了丰富的音频处理功能和一系列预处理方法,方便用户在音频领域进行机器学习和深度学习的研究。具体来说,Torchaudio提供了从音频文件的读取到加载,音频变换和增强,以及音频数据可视化的整套工具。此外,Torchaudio还集成了一些常见的音频数据集,方便用户快速获取和处理音频数据。

在安装方面,首先需要安装PyTorch,可以通过pip install torch命令来安装最新版本。然后,可以使用pip install torchaudio命令来安装Torchaudio库。

Torchaudio的读取音频文件功能通过torchaudio.load方法实现,该方法返回一个包含音频数据的waveform张量和采样率。同时,Torchaudio提供了一些常用的音频变换和增强方法,如时域混响、频域滤波等,方便用户对音频数据进行处理和增强。

总之,Torchaudio是PyTorch在音频处理领域的强大扩展,它提供了丰富的音频处理功能和预处理方法,方便用户在音频领域进行机器学习和深度学习的研究。
本教程展示如何使用 TorchAudio 的基本 I/O API 来检查音频数据、将其加载到 PyTorch Tensors 中并保存 PyTorch Tensors。
首先查看Torchaudio版本是否正确,建议使用2.1以上版本

import torch
import torchaudio

print(torch.__version__)
print(torchaudio.__version__)

输出:

2.1.1
2.1.0

准备
首先,我们导入模块并下载本教程中使用的音频资源。

请使用以下命令安装所需的包,使用pip安装:

pip install boto3
import io
import os
import tarfile
import tempfile

import boto3
import matplotlib.pyplot as plt
import requests
from botocore import UNSIGNED
from botocore.config import Config
from IPython.display import Audio
from torchaudio.utils import download_asset

SAMPLE_GSM = download_asset("tutorial-assets/steam-train-whistle-daniel_simon.gsm")
SAMPLE_WAV = download_asset("tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav")
SAMPLE_WAV_8000 = download_asset("tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042-8000hz.wav")


def _hide_seek(obj):
    class _wrapper:
        def __init__(self, obj):
            self.obj = obj

        def read(self, n):
            return self.obj.read(n)

    return _wrapper(obj)

输出:

  0%|          | 0.00/7.99k [00:00<?, ?B/s]
100%|##########| 7.99k/7.99k [00:00<00:00, 7.62MB/s]

  0%|          | 0.00/53.2k [00:00<?, ?B/s]
100%|##########| 53.2k/53.2k [00:00<00:00, 49.1MB/s]

查询音频元数据
函数torchaudio.info()获取音频元数据。您可以提供类似路径的对象或类似文件的对象。

metadata = torchaudio.info(SAMPLE_WAV)
print(metadata)
AudioMetaData(sample_rate=16000, num_frames=54400, num_channels=1, bits_per_sample=16, encoding=PCM_S)

在哪里

sample_rate是音频的采样率

num_channels是通道数

num_frames是每个通道的帧数

bits_per_sample是位深度

encoding是示例编码格式

encoding可以采用以下值之一:

“PCM_S”:有符号整数线性 PCM

“PCM_U”: 无符号整数线性 PCM

“PCM_F”:浮点线性PCM

“FLAC”:Flac,免费无损音频编解码器

“ULAW”:Mu-law,[维基百科]

“ALAW”:A-law [维基百科]

“MP3”:MP3、MPEG-1 音频第三层

“VORBIS”: OGG Vorbis [ xiph.org ]

“AMR_NB”:自适应多速率[维基百科]

“AMR_WB”:自适应多速率宽带[维基百科]

“OPUS”: 作品 [ opus-codec.org ]

“GSM”:GSM-FR [维基百科]

“HTK”:单通道16位PCM

"UNKNOWN"以上都不是

笔记

bits_per_sample可以0用于具有压缩和/或可变比特率的格式(例如 MP3)。

num_frames可以用于0GSM-FR 格式。

metadata = torchaudio.info(SAMPLE_GSM)
print(metadata)
AudioMetaData(sample_rate=8000, num_frames=39680, num_channels=1, bits_per_sample=0, encoding=GSM)

查询类文件对象
torchaudio.info()适用于类似文件的对象。

url = "https://download.pytorch.org/torchaudio/tutorial-assets/steam-train-whistle-daniel_simon.wav"
with requests.get(url, stream=True) as response:
    metadata = torchaudio.info(_hide_seek(response.raw))
print(metadata)

输出:

AudioMetaData(sample_rate=44100, num_frames=109368, num_channels=2, bits_per_sample=16, encoding=PCM_S)

传递类似文件的对象时,info不会读取所有底层数据;相反,它只从头开始读取部分数据。因此,对于给定的音频格式,它可能无法检索正确的元数据,包括格式本身。在这种情况下,您可以传递format参数来指定音频的格式。

加载音频数据

要加载音频数据,您可以使用torchaudio.load().

该函数接受类似路径的对象或类似文件的对象作为输入。

返回值是波形 ( Tensor) 和采样率 ( int) 的元组。

默认情况下,生成的张量对象具有dtype=torch.float32,其值范围为。[-1.0, 1.0]

有关支持的格式列表,请参阅torchaudio 文档。

waveform, sample_rate = torchaudio.load(SAMPLE_WAV)
def plot_waveform(waveform, sample_rate):
    waveform = waveform.numpy()

    num_channels, num_frames = waveform.shape
    time_axis = torch.arange(0, num_frames) / sample_rate

    figure, axes = plt.subplots(num_channels, 1)
    if num_channels == 1:
        axes = [axes]
    for c in range(num_channels):
        axes[c].plot(time_axis, waveform[c], linewidth=1)
        axes[c].grid(True)
        if num_channels > 1:
            axes[c].set_ylabel(f"Channel {c+1}")
    figure.suptitle("waveform")
plot_waveform(waveform, sample_rate)

波形

def plot_specgram(waveform, sample_rate, title="Spectrogram"):
    waveform = waveform.numpy()

    num_channels, num_frames = waveform.shape

    figure, axes = plt.subplots(num_channels, 1)
    if num_channels == 1:
        axes = [axes]
    for c in range(num_channels):
        axes[c].specgram(waveform[c], Fs=sample_rate)#specgram函数生成语谱图,也就是经过傅里叶变换生成频谱图
        if num_channels > 1:
            axes[c].set_ylabel(f"Channel {c+1}")
    figure.suptitle(title)
plot_specgram(waveform, sample_rate)

生成时频图,采样滑动窗口对音频数据进行傅里叶变换,因此反映了时间与频率分布的情况
频谱图

Audio(waveform.numpy()[0], rate=sample_rate)

从网络文件对象加载
I/O 函数支持类文件对象。这允许从本地文件系统内外的位置获取和解码音频数据。以下示例说明了这一点。

# Load audio data as HTTP request
url = "https://download.pytorch.org/torchaudio/tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
with requests.get(url, stream=True) as response:
    waveform, sample_rate = torchaudio.load(_hide_seek(response.raw))
plot_specgram(waveform, sample_rate, title="HTTP datasource")

HTTP数据源
在这里插入图片描述

# Load audio from tar file
tar_path = download_asset("tutorial-assets/VOiCES_devkit.tar.gz")
tar_item = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
with tarfile.open(tar_path, mode="r") as tarfile_:
    fileobj = tarfile_.extractfile(tar_item)
    waveform, sample_rate = torchaudio.load(fileobj)
plot_specgram(waveform, sample_rate, title="TAR file")

在这里插入图片描述
输出:

  0%|          | 0.00/110k [00:00<?, ?B/s]
100%|##########| 110k/110k [00:00<00:00, 76.0MB/s]
# Load audio from S3
bucket = "pytorch-tutorial-assets"
key = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
client = boto3.client("s3", config=Config(signature_version=UNSIGNED))
response = client.get_object(Bucket=bucket, Key=key)
waveform, sample_rate = torchaudio.load(_hide_seek(response["Body"]))
plot_specgram(waveform, sample_rate, title="From S3")

来自S3

切片技巧
提供num_frames和frame_offset参数将解码限制为输入的相应段。

使用普通张量切片(即 )可以实现相同的结果 。然而,提供和论证更为有效。waveform[:, frame_offset:frame_offset+num_frames]num_framesframe_offset

这是因为一旦完成对请求帧的解码,该函数将结束数据采集和解码。当音频数据通过网络传输时,这是有利的,因为一旦获取了必要的数据量,数据传输就会停止。

下面的例子说明了这一点。

# Illustration of two different decoding methods.
# The first one will fetch all the data and decode them, while
# the second one will stop fetching data once it completes decoding.
# The resulting waveforms are identical.

frame_offset, num_frames = 16000, 16000  # Fetch and decode the 1 - 2 seconds

url = "https://download.pytorch.org/torchaudio/tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
print("Fetching all the data...")
with requests.get(url, stream=True) as response:
    waveform1, sample_rate1 = torchaudio.load(_hide_seek(response.raw))
    waveform1 = waveform1[:, frame_offset : frame_offset + num_frames]
    print(f" - Fetched {response.raw.tell()} bytes")

print("Fetching until the requested frames are available...")
with requests.get(url, stream=True) as response:
    waveform2, sample_rate2 = torchaudio.load(
        _hide_seek(response.raw), frame_offset=frame_offset, num_frames=num_frames
    )
    print(f" - Fetched {response.raw.tell()} bytes")

print("Checking the resulting waveform ... ", end="")
assert (waveform1 == waveform2).all()
print("matched!")

输出:

Fetching all the data...
 - Fetched 108844 bytes
Fetching until the requested frames are available...
 - Fetched 108844 bytes
Checking the resulting waveform ... matched!

将音频保存到文件

要将音频数据保存为常见应用程序可解释的格式,您可以使用torchaudio.save().

该函数接受类似路径的对象或类似文件的对象。

当传递类似文件的对象时,您还需要提供参数,format 以便函数知道应该使用哪种格式。对于类似路径的对象,该函数将从扩展名推断格式。如果要保存到没有扩展名的文件,则需要提供 argument format。

保存WAV格式的数据时,Tensor的默认编码float32是32位浮点PCM。您可以提供参数encoding并 bits_per_sample更改此行为。例如,要将数据保存在 16 位有符号整数 PCM 中,您可以执行以下操作。

以较低位深度的编码保存数据会减少生成的文件大小,但也会降低精度。

waveform, sample_rate = torchaudio.load(SAMPLE_WAV)
def inspect_file(path):
    print("-" * 10)
    print("Source:", path)
    print("-" * 10)
    print(f" - File size: {os.path.getsize(path)} bytes")
    print(f" - {torchaudio.info(path)}")
    print()

保存时不带任何编码选项。该函数将选择所提供的数据适合的编码

with tempfile.TemporaryDirectory() as tempdir:
    path = f"{tempdir}/save_example_default.wav"
    torchaudio.save(path, waveform, sample_rate)
    inspect_file(path)

输出:

----------
Source: /tmp/tmph9zhvqey/save_example_default.wav
----------
 - File size: 108878 bytes
 - AudioMetaData(sample_rate=16000, num_frames=54400, num_channels=1, bits_per_sample=16, encoding=PCM_S)

另存为 16 位有符号整数 Linear PCM 生成的文件占用一半存储空间但会损失精度

with tempfile.TemporaryDirectory() as tempdir:
    path = f"{tempdir}/save_example_PCM_S16.wav"
    torchaudio.save(path, waveform, sample_rate, encoding="PCM_S", bits_per_sample=16)
    inspect_file(path)

输出:

----------
Source: /tmp/tmpnjohn05p/save_example_PCM_S16.wav
----------
 - File size: 108878 bytes
 - AudioMetaData(sample_rate=16000, num_frames=54400, num_channels=1, bits_per_sample=16, encoding=PCM_S)

torchaudio.save()还可以处理其他格式。仅举几例:

formats = [
    "flac",
    # "vorbis",
    # "sph",
    # "amb",
    # "amr-nb",
    # "gsm",
]
waveform, sample_rate = torchaudio.load(SAMPLE_WAV_8000)
with tempfile.TemporaryDirectory() as tempdir:
    for format in formats:
        path = f"{tempdir}/save_example.{format}"
        torchaudio.save(path, waveform, sample_rate, format=format)
        inspect_file(path)

输出

----------
Source: /tmp/tmpt4f0mdkn/save_example.flac
----------
 - File size: 45262 bytes
 - AudioMetaData(sample_rate=8000, num_frames=27200, num_channels=1, bits_per_sample=16, encoding=FLAC)

保存到类似文件的对象

与其他 I/O 功能类似,您可以将音频保存到类似文件的对象。保存到类似文件的对象时,format需要参数。

waveform, sample_rate = torchaudio.load(SAMPLE_WAV)

# Saving to bytes buffer
buffer_ = io.BytesIO()
torchaudio.save(buffer_, waveform, sample_rate, format="wav")

buffer_.seek(0)
print(buffer_.read(16))

输出:

b'RIFFF\xa9\x01\x00WAVEfmt '

完整代码



import torch
import torchaudio

print(torch.__version__)
print(torchaudio.__version__)


import io
import os
import tarfile
import tempfile

import boto3
import matplotlib.pyplot as plt
import requests
from botocore import UNSIGNED
from botocore.config import Config
from IPython.display import Audio
from torchaudio.utils import download_asset

SAMPLE_GSM = download_asset("tutorial-assets/steam-train-whistle-daniel_simon.gsm")
SAMPLE_WAV = download_asset("tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav")
SAMPLE_WAV_8000 = download_asset("tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042-8000hz.wav")


def _hide_seek(obj):
    class _wrapper:
        def __init__(self, obj):
            self.obj = obj

        def read(self, n):
            return self.obj.read(n)

    return _wrapper(obj)


metadata = torchaudio.info(SAMPLE_WAV)
print(metadata)

metadata = torchaudio.info(SAMPLE_GSM)
print(metadata)


url = "https://download.pytorch.org/torchaudio/tutorial-assets/steam-train-whistle-daniel_simon.wav"
with requests.get(url, stream=True) as response:
    metadata = torchaudio.info(_hide_seek(response.raw))
print(metadata)

waveform, sample_rate = torchaudio.load(SAMPLE_WAV)


######################################################################
#
def plot_waveform(waveform, sample_rate):
    waveform = waveform.numpy()

    num_channels, num_frames = waveform.shape
    time_axis = torch.arange(0, num_frames) / sample_rate

    figure, axes = plt.subplots(num_channels, 1)
    if num_channels == 1:
        axes = [axes]
    for c in range(num_channels):
        axes[c].plot(time_axis, waveform[c], linewidth=1)
        axes[c].grid(True)
        if num_channels > 1:
            axes[c].set_ylabel(f"Channel {c+1}")
    figure.suptitle("waveform")


######################################################################
#
plot_waveform(waveform, sample_rate)


######################################################################
#
def plot_specgram(waveform, sample_rate, title="Spectrogram"):
    waveform = waveform.numpy()

    num_channels, num_frames = waveform.shape

    figure, axes = plt.subplots(num_channels, 1)
    if num_channels == 1:
        axes = [axes]
    for c in range(num_channels):
        axes[c].specgram(waveform[c], Fs=sample_rate)
        if num_channels > 1:
            axes[c].set_ylabel(f"Channel {c+1}")
    figure.suptitle(title)


######################################################################
#
plot_specgram(waveform, sample_rate)


######################################################################
#
Audio(waveform.numpy()[0], rate=sample_rate)

# Load audio data as HTTP request
url = "https://download.pytorch.org/torchaudio/tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
with requests.get(url, stream=True) as response:
    waveform, sample_rate = torchaudio.load(_hide_seek(response.raw))
plot_specgram(waveform, sample_rate, title="HTTP datasource")

######################################################################
#

# Load audio from tar file
tar_path = download_asset("tutorial-assets/VOiCES_devkit.tar.gz")
tar_item = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
with tarfile.open(tar_path, mode="r") as tarfile_:
    fileobj = tarfile_.extractfile(tar_item)
    waveform, sample_rate = torchaudio.load(fileobj)
plot_specgram(waveform, sample_rate, title="TAR file")

######################################################################
#

# Load audio from S3
bucket = "pytorch-tutorial-assets"
key = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
client = boto3.client("s3", config=Config(signature_version=UNSIGNED))
response = client.get_object(Bucket=bucket, Key=key)
waveform, sample_rate = torchaudio.load(_hide_seek(response["Body"]))
plot_specgram(waveform, sample_rate, title="From S3")


frame_offset, num_frames = 16000, 16000  # Fetch and decode the 1 - 2 seconds

url = "https://download.pytorch.org/torchaudio/tutorial-assets/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
print("Fetching all the data...")
with requests.get(url, stream=True) as response:
    waveform1, sample_rate1 = torchaudio.load(_hide_seek(response.raw))
    waveform1 = waveform1[:, frame_offset : frame_offset + num_frames]
    print(f" - Fetched {response.raw.tell()} bytes")

print("Fetching until the requested frames are available...")
with requests.get(url, stream=True) as response:
    waveform2, sample_rate2 = torchaudio.load(
        _hide_seek(response.raw), frame_offset=frame_offset, num_frames=num_frames
    )
    print(f" - Fetched {response.raw.tell()} bytes")

print("Checking the resulting waveform ... ", end="")
assert (waveform1 == waveform2).all()
print("matched!")

waveform, sample_rate = torchaudio.load(SAMPLE_WAV)


######################################################################
#


def inspect_file(path):
    print("-" * 10)
    print("Source:", path)
    print("-" * 10)
    print(f" - File size: {os.path.getsize(path)} bytes")
    print(f" - {torchaudio.info(path)}")
    print()


######################################################################
#
# Save without any encoding option.
# The function will pick up the encoding which
# the provided data fit
with tempfile.TemporaryDirectory() as tempdir:
    path = f"{tempdir}/save_example_default.wav"
    torchaudio.save(path, waveform, sample_rate)
    inspect_file(path)

######################################################################
#
# Save as 16-bit signed integer Linear PCM
# The resulting file occupies half the storage but loses precision
with tempfile.TemporaryDirectory() as tempdir:
    path = f"{tempdir}/save_example_PCM_S16.wav"
    torchaudio.save(path, waveform, sample_rate, encoding="PCM_S", bits_per_sample=16)
    inspect_file(path)


######################################################################
# :py:func:`torchaudio.save` can also handle other formats.
# To name a few:
#

formats = [
    "flac",
    # "vorbis",
    # "sph",
    # "amb",
    # "amr-nb",
    # "gsm",
]

######################################################################
#
waveform, sample_rate = torchaudio.load(SAMPLE_WAV_8000)
with tempfile.TemporaryDirectory() as tempdir:
    for format in formats:
        path = f"{tempdir}/save_example.{format}"
        torchaudio.save(path, waveform, sample_rate, format=format)
        inspect_file(path)

######################################################################
# Saving to file-like object
# --------------------------
#
# Similar to the other I/O functions, you can save audio to file-like
# objects. When saving to a file-like object, argument ``format`` is
# required.
#


waveform, sample_rate = torchaudio.load(SAMPLE_WAV)

# Saving to bytes buffer
buffer_ = io.BytesIO()
torchaudio.save(buffer_, waveform, sample_rate, format="wav")

buffer_.seek(0)
print(buffer_.read(16))
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
### 回答1: 《PyTorch生物医学视觉深度学习入门与实战--豪华版》是一本针对生物医学领域的深度学习入门教材。它涵盖了PyTorch框架下的生物医学图像处理、分割、分类、检测等任务,并提供了相关实战案例。 这本书首先介绍了生物医学视觉深度学习的基础知识,包括卷积神经网络、深度学习模型的优化和训练等。接着,书中详细解释了PyTorch框架的基本用法和相关工具,帮助读者在实践中理解和应用深度学习算法。 在实战部分,本书结合生物医学图像处理的具体问题,介绍了数据预处理、模型构建和训练、结果评估等关键步骤。读者可以通过实例学习使用PyTorch框架处理生物医学图像,如医学图像的分割、肿瘤检测和分类等任务,并了解如何应对不同情况下的挑战。 此外,书中还介绍了一些扩展内容,如生成对抗网络(GAN)在生物医学领域的应用、迁移学习等,以帮助读者深入理解和拓展知识。 综上所述,《PyTorch生物医学视觉深度学习入门与实战--豪华版》是一本全面介绍生物医学图像处理深度学习的教材,适合对生物医学领域感兴趣的学习者,通过学习本书的内容可以提高在生物医学图像处理方面的能力和应用水平。 ### 回答2: 《PyTorch生物医学视觉深度学习入门与实战--豪华版》是一本针对深度学习在生物医学视觉领域应用的学习和实践指南。 深度学习在生物医学视觉中具有重要的应用价值,例如通过图像识别和分割等技术来辅助医生进行疾病诊断和治疗。而PyTorch作为一种强大的深度学习框架,为进行生物医学视觉深度学习提供了方便和高效的工具。 《PyTorch生物医学视觉深度学习入门与实战--豪华版》从初级到高级提供了全面而系统的学习内容。书中首先介绍了深度学习的基础知识和常用算法,以及PyTorch的基本使用方法。然后,详细探讨了如何在生物医学视觉领域中应用深度学习,包括图像分割、图像分类、目标检测等任务的具体方法和实践。 此外,《PyTorch生物医学视觉深度学习入门与实战--豪华版》还提供了大量的实际案例和代码示例,读者可以通过实践来巩固所学知识。书中也讨论了一些生物医学数据集的特点和处理方法,以及常用性能评估指标的使用。 总的来说,《PyTorch生物医学视觉深度学习入门与实战--豪华版》旨在帮助读者快速入门和应用深度学习技术解决生物医学视觉问题。无论是初学者还是有一定基础的研究人员都可以从中获得深入的学习和实践经验,从而在生物医学领域取得更好的成果。 ### 回答3: 《PyTorch生物医学视觉深度学习入门与实战--豪华版》是一本关于使用PyTorch进行生物医学图像处理和深度学习的书籍。本书旨在帮助读者从零开始了解如何使用PyTorch来处理生物医学图像,并深入学习深度学习算法在生物医学图像分析中的应用。 该书首先介绍了PyTorch这一热门的深度学习框架的基本概念和使用方法,包括Tensor、模型构建、前向传播、反向传播等。然后,书中讲解了生物医学图像处理的基础知识,如图像预处理、增强和分割等技术。 在掌握了基础知识之后,读者将逐步学习应用深度学习算法处理生物医学图像的方法。书中以一系列实战项目为例,涵盖了多个生物医学图像处理任务,如肺部结节检测、乳腺癌识别、皮肤病分类等。每个项目都介绍了该任务的背景和重要性,然后详细讲解了使用PyTorch实现的具体步骤。 此外,该书还特别强调了实践的重要性。每个实战项目都包括了详细的代码示例和完整的代码实现,读者可以根据书中的指导逐步实现项目,并通过实践加深对深度学习和生物医学图像处理的理解。 总之,《PyTorch生物医学视觉深度学习入门与实战--豪华版》是一本全面介绍如何使用PyTorch进行生物医学图像处理和深度学习的书籍。它可以帮助读者从零开始学习并掌握相关知识和技能,并通过实际项目的实战经验,进一步提升自己在生物医学视觉深度学习领域的能力。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农呆呆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值