Linux录音详解

from : http://blog.163.com/tongfangyuan0000@126/blog/static/43041855200921272753720/

1)查看声卡:

tong@tfylaptop:~/Documents/机器人舞蹈/程序/MIT/linux/tapping3(NEW)$ arecord -l

**** List of CAPTURE Hardware Devices ****
card 0: Intel [HDA Intel], device 0: STAC92xx Analog [STAC92xx Analog]
Subdevices: 3/3
Subdevice #0: subdevice #0
Subdevice #1: subdevice #1
Subdevice #2: subdevice #2

或者:

查看声卡驱动驱动安装状况:

tong@tfylaptop:~/Documents/机器人舞蹈/程序/MIT/linux/tapping3(NEW)$ cat /dev/sndstat
Sound Driver:3.8.1a-980706 (ALSA v1.0.16 emulation code)
Kernel: Linux tfylaptop 2.6.24-16-generic #1 SMP Thu Apr 10 13:23:42 UTC 2008 i686
Config options: 0

Installed drivers:
Type 10: ALSA emulation

Card config:
HDA Intel at 0xfe9fc000 irq 20

Audio devices:
0: STAC92xx Analog (DUPLEX)

Synth devices: NOT ENABLED IN CONFIG

Midi devices: NOT ENABLED IN CONFIG

Timers:
7: system timer

Mixers:
0: SigmaTel STAC9228

oss默认安转目录是 /usr/lib/oss



2)录音(时间长度为10s,保存录音文件为0.wav,plughw:0表示第一块声卡)


tong@tfylaptop:~/Documents/机器人舞蹈/程序/MIT/linux/tapping3(NEW)$ arecord -d 10 -D plughw:0 0.wav
Recording WAVE '0.wav' : Unsigned 8 bit, Rate 8000 Hz, Mono

3)


Linux下录音:

Linux的音频输入输出是通过/dev/dsp设备的,但对于这些声音信号的处理则是通过/dev/mixer设备来完成的

对Mixer文件操作所用的一些变量

可通过看linux/soundcard.h文件来获取这些变量,这里列一些基本的常用的变量
SOUND_MIXER_WRITE_VOLUME = 0xc0044d00
SOUND_MIXER_WRITE_BASS = 0xc0044d01
SOUND_MIXER_WRITE_PCM = 0xc0044d04
SOUND_MIXER_WRITE_LINE = 0xc0044d06
SOUND_MIXER_WRITE_MIC = 0xc0044d07
SOUND_MIXER_WRITE_RECSRC = 0xc0044dff
SOUND_MIXER_LINE = 7
SOUND_MASK_LINE = 64
这些变量名都是在soundcard.h中 可以查到的,通过名称即可看出其用途,后面的赋值在该头文件中则并不是这样定义的,而是通过调用一些函数返回出来的,应该是声卡上对应的地址。在应用程序 中可通过ioctl(fd,cmd,arg)来对这些变量进行赋值。其中fd即为一个打开/dev/mixer的文件指针,cmd为上面所列的这些变 量,arg既是对这些变量进行操作所需赋给的结构体或变量。


参考网站: http://www.cndw.com/tech/server/2006040534091.asp

6.3 yc2440 录放音的软硬件配置

以下涉及到的开发运行环境均为(Yc2440板上的utu-linux操作系统)选择的语言为C。选用的交叉编译器为armv4l-unknown-linux-gcc。

6.3.1 录音硬件(uda1341声卡)

Uda1341是市面上应用比较广的声卡芯片支持PCM硬件编码以及放音和录音功能。UDA1341支持IIS总线数据格式,采用位元流转换技术进行信号处理,具有可编程增益放大器(PGA)和数字自动增益控制器(AGC)。 UDA1341对外提供两组音频信号输入接口,每组包括左右2个声道。由于IIS总线只处理音频数据,因此UDA1341还内置用于传输控制信号的L3总线接口。L3接口相当于混音器控制接口,可以控制输入/输出音频信号的低音及音量大小等。而且这些功能可以通过程序调用utu-linux提供的上层接口函数很好的实现,更方便的支持特定需求的录放。

6.3.2 软件平台(utu-linux)

Utulinux操作系统是yc2440开发版提供的嵌入式linux系列操作,其内核为2.6.13版本。这样对于编程和开发来说就可以直接基于linux操作系统,而无需发太多的时间去考虑底层硬件的工作机制,很大程度上提高了工作效率。用linux系列的操作系统的一个优点就是在pc版linux上编译通过的程序,基本上在板载linux上也能测试通过。另外就是基于linux系统的大部分技术均为开源项目,可以很好的学习别人的成果。所以我们目前选择了utulinux而非wince。

6.3.3 声卡驱动

Linux上的声卡驱动分两种,一种为OSS系列,另外一种为ALSA系列。
ALSA (Advanced Linux Sound Architecture(高级Linux声音体系)的缩写) 是为声卡提供驱动的Linux内核组件。 一部分的目的是支持声卡的自动配置,以及完美的处理系统中的多个声音设备,这些目的大多都已达到。另一个声音框架JACK使用 ALSA 提供低延迟的专业级音频编辑和混音能力。ALSA是一个完全开放源代码的音频驱动程序集,而且完全兼容OSS。
OSS(Open Sound System)是 linux 平台上一个统一的音频接口, 即只要音频处理应用程序按照OSS的API来编写,那么在移植到另外一个平台时,只需要重新编译即可。值得注意的是OSS只是部分开源。
无论是选择ALSA还是OSS都是为了一个目的:将声卡抽象为一个统一的设备供linux程序员使用。我们前期测试选用的OSS系列。

6.3.3.2 安装声卡驱动

驱动源程序使用的是厂商附带的uda1341.c文件。
a、将该文件放置到sound/oss/目录下。 将bitfield.h放到include/asm-arm/plat-yc24xx/目录
b、在该目录下的Makfile文件的适当部位(和别的obj一起的地方)添加:
Obj-$(CONFIG_yC2440_UDA1341)+= yc2440_uda1341.o
以便能选择编译该文件。
c、在该目录下的kconfig文件的头部添加:
config yC2410_SND_UDA1341
tristate "yC2440 UDA1341 driver (yC2440)"
depends on SOUND_PRIME && SOUND && ARCH_yC2440
help
The UDA1341 can be found in Samsung's yC24XX
platforms. If you have a board based on one
of these. Say Y or Nhere.
If unsure, say N.
以便能在menuconfig的时候能选择到这个声卡。
d、在arch/arm/mach-yc2440/mach-smdk2440.c文件中添加iss的platform_device( 默认已经有了 )
static struct platform_device *smdk2440_devices[] __initdata = {
....
....
&s3c_device_iis,
....
....
};
e、make menuconfig 选择driver->sound->oss->uda1341 ,选择对声卡的支持。编译完毕,下载到yc2440开发板,使用madplay播放mp3文件。

6.3.3.3 测试声卡驱动是否安装成功

在linux上测试声卡是否正常非常方便,用两个简单的命令即可实现:
cat /dev/dsp >test.wav 将声音录为PCM数据
cat test.wav >/dev/dsp 将PCM数据播放

6.4 yc2440开发板下录音的实现机制

录音实现的大体流程:
A 测试声卡是否正常工作
B 通过mixer调节声卡输入和输出时的音量大小及各种效果(比较重要)
C 打开声卡设备 默认目录为/dev/dsp
D 打开声音文件
E 将wav头信息读入声音文件
F 从dsp声卡设备中读入pcm数据
G 关闭设备及保存声音文件
大体流程的伪代码分两部分给出,一部分为基于mixer编程的专门负责调节声卡的录音效果,第二部分为录音程序。
mixer混音编程

6.4.1.1 常用命令简介

声卡上的混音器由多个混音通道组成,它们可以通过驱动程序提供的设备文件/dev/mixer进行编程。对混音器的操作是通过ioctl系统调用来完成的,并且所有控制命令都由SOUND_MIXER或者MIXER开头,表1列出了常用的几个混音器控制命令:
名 称 作 用
SOUND_MIXER_VOLUME 主音量调节
SOUND_MIXER_BASS 低音控制
SOUND_MIXER_TREBLE 高音控制
SOUND_MIXER_SYNTH FM 合成器
SOUND_MIXER_PCM 主D/A转换器
SOUND_MIXER_SPEAKER PC喇叭
SOUND_MIXER_LINE 音频线输入
SOUND_MIXER_MIC 麦克风输入
SOUND_MIXER_CD CD 输入
SOUND_MIXER_IMIX 回放音量
SOUND_MIXER_ALTPCM 从D/A 转换器
SOUND_MIXER_RECLEV 录音音量
SOUND_MIXER_IGAIN 输入增益
SOUND_MIXER_OGAIN 输出增益
SOUND_MIXER_LINE1 声卡的第1输入
SOUND_MIXER_LINE2 声卡的第2输入
SOUND_MIXER_LINE3 声卡的第3输入

6.4.1.2 录音编程

Mixer部分比较简单现将代码及注释附下:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>
/* 用来存储所有可用混音设备的名称 */
const char *sound_device_names[] = SOUND_DEVICE_NAMES;
int fd; /* 混音设备所对应的文件描述符 */
int devmask, stereodevs; /* 混音器信息对应的位图掩码 */
char *name;
/* 显示命令的使用方法及所有可用的混音设备 */
void usage()
{
int i;
fprintf(stderr, "usage: %s <device> <left-gain%%> <right-gain%%>\n"
" %s <device> <gain%%>\n\n"
"Where <device> is one of:\n", name, name);
for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
if ((1 << i) & devmask) /* 只显示有效的混音设备 */
fprintf(stderr, "%s ", sound_device_names[i]);
fprintf(stderr, "\n");
exit(1);
}
int main(int argc, char *argv[])
{
int left, right, level; /* 增益设置 */
int status; /* 系统调用的返回值 */
int device; /* 选用的混音设备 */
char *dev; /* 混音设备的名称 */
int i;
name = argv[0];
/* 以只读方式打开混音设备 */
fd = open("/dev/mixer", O_RDONLY);
if (fd == -1) {
perror("unable to open /dev/mixer");
exit(1);
}
/* 获得所需要的信息 */
status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
if (status == -1)
perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
if (status == -1)
perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
/* 检查用户输入 */
if (argc != 3 && argc != 4)
usage();
/* 保存用户输入的混音器名称 */
dev = argv[1];
/* 确定即将用到的混音设备 */
for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
if (((1 << i) & devmask) && !strcmp(dev, sound_device_names[i]))
break;
if (i == SOUND_MIXER_NRDEVICES) { /* 没有找到匹配项 */
fprintf(stderr, "%s is not a valid mixer device\n", dev);
usage();
}
/* 查找到有效的混音设备 */
device = i;
/* 获取增益值 */
if (argc == 4) {
/* 左、右声道均给定 */
left = atoi(argv[2]);
right = atoi(argv[3]);
} else {
/* 左、右声道设为相等 */
left = atoi(argv[2]);
right = atoi(argv[2]);
}
/* 对非立体声设备给出警告信息 */
if ((left != right) && !((1 << i) & stereodevs)) {
fprintf(stderr, "warning: %s is not a stereo device\n", dev);
}
/* 将两个声道的值合到同一变量中 */
level = (right << 8) + left;
/* 设置增益 */
status = ioctl(fd, MIXER_WRITE(device), &level);
if (status == -1) {
perror("MIXER_WRITE ioctl failed");
exit(1);
}
/* 获得从驱动返回的左右声道的增益 */
left = level & 0xff;
right = (level & 0xff00) >> 8;
/* 显示实际设置的增益 */
fprintf(stderr, "%s gain set to %d%% / %d%%\n", dev, left, right);
/* 关闭混音设备 */
close(fd);
return 0;
}
用mixer调节声卡输入和输出时的声音效果
用armv4l-unknown-linux-gcc编译好上面的程序之后,下载到yc2440开发版,先不带任何参数执行一遍,此时会列出声卡上所有可用的混音通道:
[nnliubin@localhost sound]$ ./mixer
usage: ./mixer <device> <left-gain%> <right-gain%>
./mixer <device> <gain%>
Where <device> is one of:
vol pcm speaker line mic cd igain line1 phin video
之后就可以很方便地设置各个混音通道的增益大小了,例如下面的命令就能够将麦克风输入的左、右声道的增益分别设置为80%和90%:
[nnliubin@localhost sound]$ ./mixer mic 80 90
cd gain set to 80% / 90%

6.4.2 录音存储

因为录音部分程序比较长,所以将使用部分代码加文字的方式进行描述,并且录成的格式均为wav。
在程序前就最基本的东西明确一下。声卡上的DSP设备进行声音录制和回放的基本框架,它的功能是先录制几秒种音频数据,将其存放在内存缓冲区中,然后再进行回放,其所有的功能都是通过读写/dev/dsp设备文件来完成的。(详见核心代码)
这样录音程序就完成了,按8000hz,8位进行采样录音时间设置的为30秒,调用的为OSS的ioctl接口。
运行如下命令看效果
Cd 目录名
armv4l-unknown-linux-gcc -o soundrec sound.c
把soundrec文件下载到yc2440开发板
运行./soundrec 就能进入录音界面了

6.5 yc2440开发板下放音的实现机制

6.5.1 放音编程

为了更好的进一步检测我们的录音数据的正确性和录音效果决定写一专门播放wav文件的控制台播放器。其实录音功能实现以后,
放音功能的实现就可以类比实现了,只不过将过程反过来具体流程如下:
A 读wav文件的头,将各部分参数保存
B 通过ioctl来设置各个参数
C 将pcm数据读入/dev/dsp
完成以上功能以后,就能开发板上播放任何一首wav格式的声音文件了,以下为缩减版代码(一些和录音相当的部分就略去了)
将程序用交叉编译器编译成二进制可执行文件后,下载到yc2440开发版,就可以开始享受放音程序了。

6.6 核心代码

1 .PCM 到wav

// 8k hz,量化 16bit (也可能是8bit),单声道
//头文件
#include <iostream>
#include <string>
#include <iostream>
#include <fstream>
#include "stdio.h"
using namespace std;//unsigned long = DWORD,unsigned short = WORD
//wav头的结构如下所示:
//RIFF chunk
typedef struct { /*前面已经提到,这里省略*/
}HEADER;
//fmt sub-chunk
typedef struct { /*内容在此略去*/
}FMT;
//"data" sub-chunk
typedef struct { /*内容略去*/
}DATA;

int main()
{
//2008.8.8补充,对IMA ADPCM 的处理,把数据转化为二进制存储状态
//打开输入文件和输出文件
ifstream fin;
fin.open("ADPCM16.txt",ios::binary);
ofstream fout;
fout.open("ADPCM.txt",ios::binary);
char ADPCM16[2];
int TWO_ADPCM_BYTE//高4位和低4位放到一个字节里,
ADPCM1,ADPCM2;//分别存储高字节和低字节;
int i =0;
//输入10进制数据转化为二进制存储,以下为转化算法:
while(fin>>ADPCM16[1]>>ADPCM16[2])
{
if((int)ADPCM16[1]>81) //为a,b,c,d,e,f
ADPCM1=int(ADPCM16[1])-87;
else
ADPCM1=int(ADPCM16[1])-int('0');
if((int)ADPCM16[2]>81) //为a,b,c,d,e,f
ADPCM2=int(ADPCM16[2])-87;
else
ADPCM2=int(ADPCM16[2])-int('0');
TWO_ADPCM_BYTE = ADPCM1*16 + ADPCM2;
fout.write((char*)(&TWO_ADPCM_BYTE),1);
}
fin.close();
fout.close();*/
//2008.7.17加头程序
HEADER pcmHEADER;
FMT pcmFMT;
DATA pcmDATA;
//unsigned short m_pcmData;
char m_pcmData;
FILE *fp/*打开PCM文件*/,*fpCpy/*打开输出文件*/;
//HEADER部分;
strcpy(pcmHEADER.fccID,"RIFF");
strcpy(pcmHEADER.fccType,"WAVE");
if(fseek(fpCpy,sizeof(HEADER),SEEK_SET))//跳过HEADER的长度,以便下面继续写入wav文件的数据;
exit(0);
//初始化FMT成员数据并写入输出文件;
strcpy(pcmFMT.fccID,"fmt ");
fwrite(&pcmFMT,sizeof(FMT),1,fpCpy);
//DATA部分;
strcpy(pcmDATA.fccID,"data");
//跳过DATA的长度,开始写pcm数据部分,同时计算出数据量的大小;
fseek(fpCpy,sizeof(DATA),1);
//注:每次读入一个PCM数据(我们用的是16bit,也即2 bytes)
fread(&m_pcmData,sizeof(unsigned short),1,fp); //从.pcm中读入数据
//采样8 bit 时:
// fread(&m_pcmData,sizeof(char),1,fp); //从.pcm中读入数据
pcmDATA.dwSize=0;
//读入并存储数据
while(!feof(fp))
{
pcmDATA.dwSize+=2; //计算数据的长度;每读入一个数据,长度就加2;
//参考:size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
fwrite(&m_pcmData,sizeof(unsigned short),1,fpCpy);
fread(&m_pcmData,sizeof(unsigned short),1,fp);
//音频采样大小为8 bit 时
//pcmDATA.dwSize+=1;
//fwrite(&m_pcmData,sizeof(char),1,fpCpy);
//fread(&m_pcmData,sizeof(char),1,fp);
}
fclose(fp); //关闭文件
pcmHEADER.dwSize=36+pcmDATA.dwSize;
rewind(fpCpy); //将fpCpy变为.wav的头,以便于写入HEADER和DATA;
fwrite(&pcmHEADER,sizeof(HEADER),1,fpCpy); //写入HEADER
fseek(fpCpy,sizeof(FMT),1); //跳过FMT,因为FMT已经写入
fwrite(&pcmDATA,sizeof(DATA),1,fpCpy); //写入DATA;
fclose(fpCpy); //关闭文件
return 0;

2 .yc2440开发板下录音

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <linux/soundcard.h>
#include <string.h>
#define LENGTH 30 /* 录音时间 */
#define RATE 8000 /* 采样频率 */
#define SIZE 8 /* 采样位数 */
#define CHANNELS 1 /* 声道数*/

/* 用于保存数字音频数据的内存缓冲区 */
unsigned char buf[LENGTH*RATE*SIZE/8];
typedef struct{/*以下代码已略去*/
//用该结构来保存wav头部分里面的前10个字节
}HEADER;
//fmt 部分
typedef struct {/*以下代码已略去*/
//用该结构来保存wav头部分里面的fmt块
}FMT;
typedef struct {/*以下代码已略去*/
//用该结构来存储wav头部分里面的data块
}DATA;
int main()
{
int fd; /* 声音设备的文件描述符 */
int arg; /* 用于ioctl调用的参数*/
int status; /* 系统调用的返回值*/
FILE* out; /* 输出的声音文件*/
int saveSize=LENGTH*RATE*SIZE/8; /* 数据大小*/
HEADER pcmHEADER;
FMT pcmFMT;
DATA pcmDATA;
//unsigned short m_pcmData;
char m_pcmData;
/* 打开声音设备 */
fd = open("/dev/dsp", O_RDWR);
if (fd < 0) {
perror("open of /dev/dsp failed");
exit(1);
}
/* 设置采样时的量化位数*/
arg = SIZE;
status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
// printf("MIC gain is at %d %%\n",vol);
if (status == -1)
perror("SOUND_PCM_WRITE_BITS ioctl failed :");
if (arg != SIZE)
perror("unable to set sample size");
/* 设置采样时的声道数 */
arg = CHANNELS;
status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
if (status == -1)
perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
if (arg != CHANNELS)
perror("unable to set number of channels");
/* 设置采样时的采样频率*/
arg = RATE;
status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
if (status == -1)
perror("SOUND_PCM_WRITE_WRITE ioctl failed");
printf("Say something:\n");
status = read(fd, buf, sizeof(buf)); /* 将数据读入缓冲区 */
if (status != sizeof(buf))
perror("read wrong number of bytes");
printf("Save as test.wav:\n");
if((out=fopen("./test.wav","wb"))==NULL)
{ printf("cannot open outfile\n");}
//初始化header部分
/*该部分代码以略去*/
if(fseek(out,sizeof(HEADER),SEEK_SET))
exit(0);
//初始化Fmt部分
/*该部分代码已略去*/
//将FMT写入wav文件中
fwrite(&pcmFMT,sizeof(FMT),1,out);
//DATA部分初始化
/*该部分代码已略去*/
fwrite(buf, sizeof(char),saveSize,out); /*存至wav中*/
rewind(out); /*将文件指针定位到文件头*/
fwrite(&pcmHEADER,sizeof(HEADER),1,out); /*写入header部分*/
fseek(out,sizeof(FMT),1);
fwrite(&pcmDATA,sizeof(DATA),1,out); /*写入data部分*/
fclose(out);
close(fd); /*关闭设备录音结束*/
}

3 yc2440开发板下放音

#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>


#include <linux/soundcard.h>
#define BUFSIZE 512 //定义缓冲区大小
/*和录音一样定义 header,fmt data 三种数据结构来
保存wav的头信息 */
int main(int argc, char *argv[])
{
/*定义所需的各种变量与录音类似*/
struct RIFF_Header riff_header;
struct Chunk_Header fmt_chunk, data_chunk;
struct Wave_Format wavfmt;

char buf[BUFSIZE];
FILE * fwave;
//和录音一样定义各种放音参数
int sndfd, status, arg, readbytes, writebytes, writed;
if( argc < 2 ){ //如果没有带命令行参数报错
fprintf(stderr, "Usage: wavplay <filename>\n");
exit(-1);
}
fwave = fopen( argv[1], "r"); //以写的方式打开命令行参数里所带的声音文件
if( fwave == NULL ){
fprintf(stderr, "Can't open file %s\n", argv[1]);
exit(-1);
}
fread(&riff_header, 1, sizeof(struct RIFF_Header), fwave);
if( strncmp(riff_header.RIFF_ID, "RIFF", 4) || strncmp(riff_header.RIFF_Format, "WAVE",4)){ 如果不满足wav文件的标准定义这里显示不可识别的文件形式
fprintf(stderr, "Unknown file format.\n");
exit(-1);
}
sndfd = open("/dev/dsp", O_RDWR);
if (sndfd < 0) { //以读写方式打开声音设备
perror("open of /dev/dsp failed");
exit(-1);
}
fread(&fmt_chunk, 1, sizeof(struct Chunk_Header), fwave);
if( !strncmp(fmt_chunk.Chunk_ID, "fmt ", 4) ){
//将fmt chunk部分参数读进来并保存
/*该部分代码因与录音类似以略去*/
}else{ //如果不是标准的格式报错
fprintf(stderr, "Can't find fmt chunk.\n");
exit(-1);
}
while( fread(&data_chunk, 1, sizeof(struct Chunk_Header), fwave) != 0 )
if( !strncmp(data_chunk.Chunk_ID, "data", 4) ){
//读wav文件里面的PCM数据
printf("Begin Play\n");
/* data chunk */
writed = 0;
while(writed < data_chunk.Chunk_Size){
//当pcm数据未处理完时继续处理
readbytes = fread(buf, 1, BUFSIZE, fwave); //从声音文件读数据
writebytes = write(sndfd, buf, readbytes); //将数据写入sndfd声卡设备实施放音
if( writebytes != readbytes )
perror("wrote wrong number of bytes");
writed += readbytes;
}
}else{
/* 格式不对掠过 */
fseek(fwave, data_chunk.Chunk_Size + fmt_chunk.Chunk_Size%2, SEEK_CUR);
}
fclose(fwave); //关闭各设备
close(sndfd);
return 0;
}
}

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值