C语言文件操作进阶:实现音频日志的本地存储与Qwen3-ASR批量处理
C语言文件操作进阶:实现音频日志的本地存储与Qwen3-ASR批量处理
你是不是也遇到过这样的烦恼?手机里、电脑里存了一堆会议录音、访谈音频,想整理成文字,一个个上传到在线工具去识别,费时又费力。更别提有些音频还涉及隐私,不方便上传到云端。
今天,咱们就来解决这个痛点。我会手把手带你,用最经典的C语言,打造一个属于你自己的本地音频处理小工具。这个工具能干嘛呢?简单说就是三件事:自动扫描你电脑里某个文件夹的所有音频文件、调用一个强大的本地语音识别模型(Qwen3-ASR-0.6B)把它们转成文字、最后把音频信息和识别结果整整齐齐地保存到本地文件里。整个过程完全在你自己电脑上运行,数据不出门,安全又高效。
学完这篇,你不仅能掌握C语言操作文件的那些“高级”技巧,还能亲手做出一个真正有用的程序,用来处理你积压的音频资料库。咱们不搞虚的,直接上代码,开干!
1. 工具准备与环境搭建
工欲善其事,必先利其器。在开始写代码之前,咱们得先把“厨房”收拾好,把需要的“食材”和“灶具”备齐。
首先,你需要一个写C语言的环境。这个很简单,Windows上可以用Dev-C++、Code::Blocks或者Visual Studio;Linux或Mac上直接用GCC编译器就行。我后面演示的代码会尽量用标准C库,保证在各个平台都能编译通过。
接下来是核心“食材”——Qwen3-ASR-0.6B模型。这是一个可以在本地运行的语音识别模型,大小适中,效果不错。你需要通过CSDN星图镜像广场这样的平台,找到对应的镜像并完成本地部署。部署成功后,这个模型通常会提供一个HTTP API接口,比如http://localhost:8000/v1/audio/transcriptions。我们的C程序就是通过向这个地址发送音频文件,来获取识别文本的。请确保在运行我们的工具之前,这个语音识别服务已经正常启动。
最后,规划一下我们的“工作区”。假设我们在桌面上新建一个项目文件夹,叫audio_logger。里面可以再建几个子文件夹:
audio_src/:用来存放待处理的原始音频文件,比如.wav,.mp3格式的。log_output/:用来存放我们程序生成的日志和识别结果文件。
环境准备好了,思路也清晰了:程序从audio_src/读音频,调用API识别,结果存到log_output/。接下来,我们就进入核心的C语言编程部分。
2. C语言文件与目录操作核心代码
这一节是咱们工具的“骨架”。一个本地文件管理工具,最基本的能力就是能遍历文件夹、能读取文件信息。我们用C标准库里的和头文件提供的函数来实现。
2.1 扫描目录,获取音频文件列表
我们的目标是扫描audio_src目录,找出里面所有的.wav和.mp3文件。在C语言里,这需要用到目录流操作。
#include <stdio.h> #include <dirent.h> #include <string.h> #include <sys/stat.h> // 定义最大文件路径长度和最大文件数 #define MAX_PATH 512 #define MAX_FILES 100 // 存储文件信息的结构体 typedef struct { char filepath[MAX_PATH]; // 完整路径 char filename[256]; // 文件名 long size; // 文件大小(字节) time_t modify_time; // 最后修改时间 } AudioFileInfo; // 扫描目录函数 int scan_audio_directory(const char *dir_path, AudioFileInfo file_list[], int *count) { DIR *dir; struct dirent *entry; struct stat file_stat; char full_path[MAX_PATH]; dir = opendir(dir_path); if (dir == NULL) { perror("无法打开目录"); return -1; } *count = 0; while ((entry = readdir(dir)) != NULL && *count < MAX_FILES) { // 跳过当前目录(.)和上级目录(..) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 拼接完整路径 snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); // 获取文件状态信息 if (stat(full_path, &file_stat) == -1) { continue; // 获取失败,跳过此文件 } // 判断是否是普通文件,并且是音频文件(简单通过后缀判断) if (S_ISREG(file_stat.st_mode)) { char *ext = strrchr(entry->d_name, '.'); if (ext != NULL && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".mp3") == 0)) { // 保存文件信息到列表 strncpy(file_list[*count].filepath, full_path, MAX_PATH - 1); strncpy(file_list[*count].filename, entry->d_name, 255); file_list[*count].size = file_stat.st_size; file_list[*count].modify_time = file_stat.st_mtime; (*count)++; } } } closedir(dir); return 0; // 成功 }这段代码干了啥?我简单解释一下。scan_audio_directory函数就像派了一个小助手进到指定的文件夹(dir_path)。小助手打开文件夹(opendir),然后挨个查看里面的每一项(readdir)。对于每一项,它先跳过.和..这两个特殊的目录,然后拼出这个文件的完整路径。
接着,它用stat函数获取文件的详细信息,比如大小、修改时间。最关键的一步是判断文件类型:首先得是普通文件(不是文件夹),其次文件名后缀得是.wav或.mp3。符合条件的文件,小助手就把它的路径、名字、大小、修改时间记录到一个清单(file_list数组)里,最后把文件夹关上(closedir)。
这样,我们就把目标目录下的音频文件都找出来了,并且把关键信息存好了。你可以写个简单的main函数调用它试试看,打印出找到的文件列表。
2.2 读取音频文件元信息(进阶)
上面的代码已经获取了文件大小和修改时间。有时候,我们可能还想知道音频文件更专业的信息,比如时长、采样率、声道数。这些信息存储在音频文件的“头部”。完全用C标准库解析这些格式(如WAV的RIFF头、MP3的ID3标签)比较繁琐,但对于学习来说,我们可以尝试解析最简单的WAV格式。
下面是一个简化版的WAV头解析函数,帮助你理解原理:
#include <stdint.h> // 用于明确大小的整数类型 // WAV文件头结构(简化版,只包含必要字段) typedef struct { char chunkID[4]; // 应为"RIFF" uint32_t chunkSize; // 文件总大小减8字节 char format[4]; // 应为"WAVE" char subchunk1ID[4]; // 应为"fmt " uint32_t subchunk1Size; // fmt块的大小,16表示PCM uint16_t audioFormat; // 音频格式,1表示PCM uint16_t numChannels; // 声道数 uint32_t sampleRate; // 采样率(Hz) uint32_t byteRate; // 每秒数据字节数 uint16_t blockAlign; // 每个采样帧的字节数 uint16_t bitsPerSample; // 位深度 // 注意:这里之后应该是"data"块,我们暂时不读数据部分 } WavHeader; int read_wav_header(const char *filepath, WavHeader *header) { FILE *file = fopen(filepath, "rb"); // 以二进制只读模式打开 if (file == NULL) { perror("无法打开WAV文件"); return -1; } // 读取头信息 size_t read_count = fread(header, sizeof(WavHeader), 1, file); fclose(file); if (read_count != 1) { fprintf(stderr, "读取WAV头信息失败\n"); return -1; } // 简单验证(实际应用需要更严谨的验证和字节序转换) if (memcmp(header->chunkID, "RIFF", 4) != 0 || memcmp(header->format, "WAVE", 4) != 0) { fprintf(stderr, "不是有效的WAV文件\n"); return -1; } // 计算近似时长(秒)= 数据大小 / 字节率 // 注意:这里需要找到并读取“data”块的大小,才是真正的音频数据长度 // 为了简化,我们假设header后紧跟data块,且使用公式:时长 ≈ (文件总大小 - 头部偏移) / byteRate // 更准确的解析需要遍历chunk,这里仅作示意。 printf("采样率:%u Hz, 声道数:%u, 位深度:%u\n", header->sampleRate, header->numChannels, header->bitsPerSample); return 0; }这段代码展示了如何打开一个WAV文件,并按照其标准格式读取最前面的44个字节左右的头信息。读出来的结构体里,就有我们关心的采样率、声道数等信息。请注意,这只是一个教学示例,真实的WAV文件可能有额外的块,并且涉及到字节序(大端/小端)的问题,生产环境的代码需要更健壮。
对于MP3或其他格式,解析会更复杂。在实际项目中,如果需要对多种音频格式进行深度解析,可以考虑集成开源的音频库,比如libsndfile。但对我们这个工具的核心目标——批量处理来说,知道文件名和路径,已经足够调用后面的识别API了。
3. 集成语音识别与批量处理逻辑
“骨架”搭好了,现在要给工具装上“大脑”和“手臂”——也就是调用语音识别API并组织批量任务的逻辑。C语言本身不擅长处理HTTP请求和JSON,但我们可以用一些库来帮忙,或者采用更直接的系统调用方式。这里我介绍一种实用的方法:通过调用curl命令行工具来发送请求。
3.1 封装语音识别API调用
假设你的Qwen3-ASR服务已经在http://localhost:8000运行,并提供了一个接收音频文件的API。我们可以写一个函数,通过popen执行curl命令来调用它。
#include <stdlib.h> #define API_URL "http://localhost:8000/v1/audio/transcriptions" #define MAX_RESULT_LEN 4096 // 调用语音识别API,将结果写入提供的缓冲区 int call_asr_api(const char *audio_file_path, char *result_text, int result_max_len) { char command[1024]; // 构建curl命令。这里假设API接受multipart/form-data格式的文件上传。 // -F 表示表单上传,-s 表示静默模式(不显示进度),-X POST 指定POST方法。 snprintf(command, sizeof(command), "curl -s -X POST \"%s\" -F \"file=@%s\" -F \"model=whisper-1\"", API_URL, audio_file_path); FILE *fp = popen(command, "r"); // 执行命令并读取其输出 if (fp == NULL) { perror("执行curl命令失败"); return -1; } // 读取API返回的JSON结果(简化处理,假设返回是纯文本或简单JSON) // 注意:真实情况需要解析JSON,这里我们假设返回格式是 `{"text": "识别结果"}` // 我们用一个临时文件来存储原始输出,然后简单提取。 // 更健壮的做法是使用如 cJSON 这样的库来解析。 char raw_result[MAX_RESULT_LEN * 2] = {0}; size_t total_read = fread(raw_result, 1, sizeof(raw_result) - 1, fp); pclose(fp); if (total_read == 0) { fprintf(stderr, "API调用未返回数据或失败。命令:%s\n", command); return -1; } // 极其简单的“解析”:寻找 "text": 后面的内容 // 警告:这非常脆弱,仅用于演示!真实项目务必使用JSON解析器。 char *text_start = strstr(raw_result, "\"text\":\""); if (text_start == NULL) { // 可能API返回了错误信息,或者格式不符 fprintf(stderr, "无法在API响应中找到文本。原始响应:%.500s...\n", raw_result); strncpy(result_text, "[识别失败:响应格式异常]", result_max_len - 1); result_text[result_max_len - 1] = '\0'; return 0; // 返回0表示本文件处理结束(尽管失败),继续下一个 } text_start += 8; // 跳过 "\"text\":\"" char *text_end = strchr(text_start, '\"'); if (text_end == NULL) { text_end = raw_result + strlen(raw_result); } int copy_len = text_end - text_start; if (copy_len >= result_max_len) { copy_len = result_max_len - 1; } strncpy(result_text, text_start, copy_len); result_text[copy_len] = '\0'; printf("文件 %s 识别成功,结果长度:%d\n", audio_file_path, copy_len); return 0; }这个函数call_asr_api的工作流程很清晰:它把目标音频文件的路径和API地址拼成一个curl命令字符串,然后通过popen在系统里执行这个命令。curl会负责把音频文件上传到你的本地语音识别服务。服务识别完后,会返回一段JSON文本,curl将其输出,我们的程序再通过fread从管道里把这段输出读出来。
这里有一个非常重要的提醒:代码里用strstr简单查找"text":来提取结果,这在实际中非常不可靠,因为JSON里可能有转义字符,格式也可能微调。这只是为了让你快速理解流程。正确的做法是引入一个C语言的JSON解析库,比如cJSON,来稳健地解析返回的数据。你可以很容易地在网上找到cJSON的用法,用它来解析会专业得多。
3.2 组织批量处理流程
有了扫描目录的函数和调用单个API的函数,我们就可以把它们串起来了。这就是我们工具的“主控程序”。
int main() { const char *audio_dir = "./audio_src"; const char *output_dir = "./log_output"; AudioFileInfo file_list[MAX_FILES]; int file_count = 0; // 1. 扫描音频目录 printf("开始扫描目录:%s\n", audio_dir); if (scan_audio_directory(audio_dir, file_list, &file_count) != 0) { fprintf(stderr, "扫描目录失败,程序退出。\n"); return 1; } printf("共找到 %d 个音频文件。\n", file_count); if (file_count == 0) { printf("没有找到可处理的音频文件。\n"); return 0; } // 2. 创建输出目录(如果不存在) // 这里使用system命令调用mkdir,注意跨平台兼容性。 char mkdir_cmd[256]; snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s", output_dir); system(mkdir_cmd); // 3. 打开或创建日志文件 char log_file_path[MAX_PATH]; snprintf(log_file_path, sizeof(log_file_path), "%s/audio_transcription_log.txt", output_dir); FILE *log_file = fopen(log_file_path, "a"); // 以追加模式打开 if (log_file == NULL) { perror("无法打开日志文件"); return 1; } // 写入日志头 fprintf(log_file, "=== 音频转录批处理日志 ===\n"); fprintf(log_file, "开始时间:%s", ctime(&(time_t){time(NULL)})); fprintf(log_file, "=================================\n\n"); // 4. 循环处理每个音频文件 for (int i = 0; i < file_count; i++) { printf("\n[%d/%d] 正在处理:%s\n", i+1, file_count, file_list[i].filename); char asr_result[MAX_RESULT_LEN] = {0}; int ret = call_asr_api(file_list[i].filepath, asr_result, MAX_RESULT_LEN); // 5. 将结果结构化保存到日志文件 fprintf(log_file, "【文件%d】\n", i+1); fprintf(log_file, " 文件名: %s\n", file_list[i].filename); fprintf(log_file, " 文件大小: %ld 字节\n", file_list[i].size); fprintf(log_file, " 修改时间: %s", ctime(&file_list[i].modify_time)); fprintf(log_file, " 识别结果: %s\n", asr_result); fprintf(log_file, " 处理状态: %s\n\n", (ret == 0) ? "成功" : "失败或部分成功"); fflush(log_file); // 及时刷新缓冲区,防止内容丢失 // 简单延时,避免过快请求对本地服务造成压力(可选) sleep(1); } // 6. 收尾工作 fprintf(log_file, "\n=================================\n"); fprintf(log_file, "批处理完成。总计处理文件:%d 个。\n", file_count); fprintf(log_file, "结束时间:%s", ctime(&(time_t){time(NULL)})); fclose(log_file); printf("\n所有文件处理完成!日志已保存至:%s\n", log_file_path); return 0; }主函数main的逻辑是一条清晰的流水线:
- 扫描:调用
scan_audio_directory,把audio_src文件夹里的音频文件信息捞出来。 - 准备:确保输出目录
log_output存在,并在里面创建一个文本文件作为我们的“工作日志”。 - 循环处理:对于找到的每一个音频文件,调用
call_asr_api函数,让它去识别。 - 记录:把每个文件的“档案”(文件名、大小、时间)和它的“识别结果”一起,工工整整地写进日志文件。
- 收尾:处理完所有文件后,在日志末尾做个总结,然后关闭文件。
这样,一个完整的本地音频批量识别和日志记录工具就完成了。你运行这个程序,它就会自动帮你把一整个文件夹的音频都转成文字,并且把所有信息都保存下来。
4. 运行示例与效果查看
理论讲完了,代码也写好了,是时候看看它实际跑起来是什么样子了。我们来模拟一个完整的运行过程。
首先,确保你的项目目录结构是这样的:
audio_logger/ ├── audio_src/ │ ├── meeting_20250410.wav │ ├── interview_part1.mp3 │ └── lecture_sample.wav ├── log_output/ (程序运行后自动创建) └── audio_processor.c (我们的主程序代码)编译并运行程序:
# 在Linux/Mac下使用GCC编译 gcc -o audio_processor audio_processor.c ./audio_processor如果一切顺利,你会在终端看到类似这样的输出:
开始扫描目录:./audio_src 共找到 3 个音频文件。 [1/3] 正在处理:meeting_20250410.wav 文件 ./audio_src/meeting_20250410.wav 识别成功,结果长度:245 [2/3] 正在处理:interview_part1.mp3 文件 ./audio_src/interview_part1.mp3 识别成功,结果长度:512 [3/3] 正在处理:lecture_sample.wav 文件 ./audio_src/lecture_sample.wav 识别成功,结果长度:189 所有文件处理完成!日志已保存至:./log_output/audio_transcription_log.txt现在,打开生成的日志文件./log_output/audio_transcription_log.txt,你会看到一份结构清晰的记录:
=== 音频转录批处理日志 === 开始时间:Thu Apr 10 15:30:22 2025 ================================= 【文件1】 文件名: meeting_20250410.wav 文件大小: 1024000 字节 修改时间: Thu Apr 10 10:15:33 2025 识别结果: 好的,那我们开始今天的周会。首先回顾一下上周各项目的进展... 处理状态: 成功 【文件2】 文件名: interview_part1.mp3 文件大小: 2048000 字节 修改时间: Wed Apr 9 14:22:10 2025 识别结果: 请问您是如何看待当前人工智能在行业中的应用趋势?我认为... 处理状态: 成功 【文件3】 文件名: lecture_sample.wav 文件大小: 1536000 字节 修改时间: Tue Apr 8 09:45:07 2025 识别结果: 今天我们讲第三章,关于文件系统的底层原理... 处理状态: 成功 ================================= 批处理完成。总计处理文件:3 个。 结束时间:Thu Apr 10 15:32:05 2025看,是不是很清晰?每个文件什么时候处理的、它本身的信息、以及最重要的识别内容,都一目了然地保存在本地了。你可以随时打开这个文本文件查看、搜索,或者把它导入到其他文档、数据库里做进一步分析。这个日志文件本身就是一个小型的、结构化的本地“数据库”。
5. 总结与后续优化思路
走完这一趟,我们不仅用C语言实现了文件的遍历、信息的读取、外部程序的调用,更重要的是,我们把这些知识点串联起来,做出了一个能解决实际问题的工具。它可能看起来不花哨,但非常实用,尤其适合处理那些需要隐私保护或网络不便的大量离线音频数据。
回顾一下整个过程,核心就是三步:找文件、识语音、存结果。代码虽然不长,但涉及了C语言中几个关键且实用的部分:目录操作、文件属性获取、系统命令调用以及文本文件的格式化写入。
当然,这个工具现在还是一个“原型”,有很大的优化空间。如果你有兴趣让它变得更强大、更健壮,可以从这几个方向试试:
- 用真正的JSON库:把那个脆弱的
strstr解析替换成cJSON库,这样无论API返回的JSON格式有什么变化,我们都能稳稳地提取出“text”字段。 - 加入错误重试机制:网络请求或API服务偶尔可能会失败。可以在
call_asr_api函数里加个循环,比如失败后等2秒再试一次,最多试3次。 - 支持更多音频格式:现在的扫描只认
.wav和.mp3。你可以很容易地扩展scan_audio_directory函数里的判断条件,加入.m4a,.flac,.ogg等后缀。 - 输出更丰富的格式:除了纯文本日志,你还可以把结果写成CSV格式,方便用Excel打开分析;或者写成JSON Lines格式,每行一个JSON对象,更容易被其他编程语言处理。
- 添加进度保存:如果处理的文件非常多,程序中途崩溃了,重新开始会很麻烦。可以设计一个“进度文件”,每成功处理一个文件,就把它的文件名记下来。下次启动时,先读这个进度文件,跳过已经处理过的。
编程最有意思的地方,就是把想法一步步变成现实,再不断打磨它。希望这个小小的音频日志工具,能成为你C语言学习路上一个有趣的实践项目,也能真正帮你省下一些整理音频的时间。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
