如何向Virtual Audio Cable写入自定义音频数据

如何向Virtual Audio Cable写入自定义音频数据

前言:什么是Virtual Audio Cable?

在数字音频处理的世界中,Virtual Audio Cable(虚拟音频线) 扮演着"音频路由器"的角色,让音频数据可以在不同的应用程序和设备之间自由流动。

为什么需要虚拟音频线?

  • 音频录制与流媒体:将游戏音效、音乐播放器和语音聊天的音频分开处理
  • 专业音频处理:将音频从一个应用程序发送到专业的音频编辑软件
  • 自动化测试:为音频设备生成测试信号
  • 辅助功能:为听力障碍用户提供音频处理通道

一、准备工作:安装Virtual Audio Cable

下载与安装

首先需要获取Virtual Audio Cable软件。本文使用的是开源版本:

bash 复制代码
下载链接:https://github.com/derek-free/virtual-audio-cable/releases/download/v4.65/vac4.65.tar

安装完成后,系统中会新增一个虚拟音频设备,通常显示为"Line 1 (Virtual Audio Cable)"或类似名称。

这个设备就像物理音频线的虚拟版本,一端是"输入",另一端是"输出"。

二、如何向VAC写入音频数据

1、音频基础

在深入代码之前,我们需要了解几个关键概念:

  1. 采样率(Sample Rate):音频信号的采集频率,44.1kHz是CD音质标准
  2. 采样深度(Bits per Sample):每个采样点的精度,16位提供65,536个可能的振幅值
  3. 声道数(Channels):立体声(2个声道)或单声道(1个声道)
  4. 缓冲区(Buffer):临时存储音频数据的内存区域

2、代码实现

以下是向Virtual Audio Cable写入音频的完整C++代码实现:

c 复制代码
#include <windows.h>
#include <mmsystem.h>
#include <mmreg.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <vector>
#include <algorithm>
#include <string>

// 音频参数配置
const int SAMPLE_RATE = 44100;          // 采样率 44.1kHz
const int BITS_PER_SAMPLE = 16;         // 16位采样深度
const int NUM_CHANNELS = 2;             // 立体声
const int BUFFER_DURATION_MS = 100;     // 每个缓冲区时长(毫秒)
const int NUM_BUFFERS = 4;              // 缓冲区数量
const float NOISE_AMPLITUDE = 0.3f;     // 噪声幅度 (0.0 ~ 1.0)

// 全局变量
HWAVEOUT g_hWaveOut = NULL;
bool g_bPlaying = false;
DWORD g_dwBytesPerSecond = 0;

// 缓冲区结构
struct AudioBuffer {
    WAVEHDR header;
    std::vector<BYTE> data;
    bool inUse;
};

std::vector<AudioBuffer> g_buffers;

// 查找 Virtual Audio Cable 设备
int FindVACDevice() {
    int deviceCount = waveOutGetNumDevs();
    printf("系统中有 %d 个音频输出设备\n", deviceCount);
    
    for (int i = 0; i < deviceCount; i++) {
        WAVEOUTCAPSW caps;
        MMRESULT result = waveOutGetDevCapsW(i, &caps, sizeof(caps));
        
        if (result == MMSYSERR_NOERROR) {
            std::wstring deviceName(caps.szPname);
            printf("设备 %d: %ws\n", i, deviceName.c_str());
            
            // 查找包含 "CABLE Input" 的设备名
            if (deviceName.find(L"CABLE Input") != std::wstring::npos ||
                deviceName.find(L"Virtual Audio Cable") != std::wstring::npos ||
                deviceName.find(L"VB-Audio Virtual Cable") != std::wstring::npos) {
                printf("找到 Virtual Audio Cable 设备: %ws (索引: %d)\n", 
                       deviceName.c_str(), i);
                return i;
            }
        }
    }
    
    printf("未找到 Virtual Audio Cable 设备,将使用默认设备\n");
    return WAVE_MAPPER;  // 使用默认设备
}

// 生成随机噪声数据
void GenerateNoise(BYTE* buffer, DWORD bufferSize) {
    int samples = bufferSize / (BITS_PER_SAMPLE / 8);    
    if (BITS_PER_SAMPLE == 16) {
        short* pSample = reinterpret_cast<short*>(buffer);        
        for (DWORD i = 0; i < samples; i++) {
            // 生成 -32768 到 32767 之间的随机数
            short noise = static_cast<short>(
                (rand() % 65536 - 32768) * NOISE_AMPLITUDE);
            *pSample++ = noise;
        }
    } else if (BITS_PER_SAMPLE == 8) {
        for (DWORD i = 0; i < bufferSize; i++) {
            buffer[i] = static_cast<BYTE>(rand() % 256);
        }
    }
}

// WaveOut 回调函数
void CALLBACK WaveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, 
                         DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
    if (uMsg == WOM_DONE) {
        WAVEHDR* pHeader = reinterpret_cast<WAVEHDR*>(dwParam1);        
        // 查找是哪个缓冲区
        for (size_t i = 0; i < g_buffers.size(); i++) {
            if (&g_buffers[i].header == pHeader) {
                if (g_bPlaying) {
                    // 重新填充数据并再次播放
                    GenerateNoise(g_buffers[i].data.data(), 
                                 static_cast<DWORD>(g_buffers[i].data.size()));                    
                    // 重新提交缓冲区
                    waveOutWrite(hwo, pHeader, sizeof(WAVEHDR));
                } else {
                    g_buffers[i].inUse = false;
                }
                break;
            }
        }
    }
}

// 初始化音频设备
bool InitializeAudio() {
    // 初始化随机数种子
    srand(static_cast<unsigned int>(time(NULL)));
    
    // 设置音频格式
    WAVEFORMATEX wfx = {0};
    wfx.wFormatTag = WAVE_FORMAT_PCM;
    wfx.nChannels = NUM_CHANNELS;
    wfx.nSamplesPerSec = SAMPLE_RATE;
    wfx.wBitsPerSample = BITS_PER_SAMPLE;
    wfx.nBlockAlign = (wfx.nChannels * wfx.wBitsPerSample) / 8;
    wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
    
    g_dwBytesPerSecond = wfx.nAvgBytesPerSec;
    
    // 查找 VAC 设备
    int deviceId = FindVACDevice();
    
    // 打开 WaveOut 设备
    MMRESULT result = waveOutOpen(&g_hWaveOut, deviceId, &wfx,
                                 reinterpret_cast<DWORD_PTR>(WaveOutProc),
                                 0, CALLBACK_FUNCTION);
    
    if (result != MMSYSERR_NOERROR) {
        printf("打开音频设备失败! 错误代码: %d\n", result);
        return false;
    }
    
    printf("音频设备初始化成功\n");
    printf("格式: %d Hz, %d 位, %d 声道\n", 
           SAMPLE_RATE, BITS_PER_SAMPLE, NUM_CHANNELS);
    
    // 计算缓冲区大小
    DWORD bufferSize = g_dwBytesPerSecond * BUFFER_DURATION_MS / 1000;
    bufferSize = (bufferSize + 3) & ~3;  // 4字节对齐
    
    printf("每个缓冲区大小: %d 字节 (%.1f 毫秒)\n", 
           bufferSize, BUFFER_DURATION_MS);
    
    // 创建缓冲区
    g_buffers.resize(NUM_BUFFERS);
    
    for (int i = 0; i < NUM_BUFFERS; i++) {
        g_buffers[i].data.resize(bufferSize);
        GenerateNoise(g_buffers[i].data.data(), bufferSize);
        
        // 初始化 WAVEHDR
        ZeroMemory(&g_buffers[i].header, sizeof(WAVEHDR));
        g_buffers[i].header.lpData = reinterpret_cast<LPSTR>(g_buffers[i].data.data());
        g_buffers[i].header.dwBufferLength = bufferSize;
        g_buffers[i].header.dwFlags = 0;
        g_buffers[i].inUse = false;
        
        // 准备缓冲区
        result = waveOutPrepareHeader(g_hWaveOut, &g_buffers[i].header, sizeof(WAVEHDR));
        if (result != MMSYSERR_NOERROR) {
            printf("准备缓冲区 %d 失败! 错误代码: %d\n", i, result);
            return false;
        }
    }
    
    printf("创建了 %d 个音频缓冲区\n", NUM_BUFFERS);
    return true;
}

// 开始播放
void StartPlayback() {
    if (!g_hWaveOut || g_bPlaying) return;
    
    g_bPlaying = true;

		// 提交所有缓冲区开始播放
		for (int i = 0; i < NUM_BUFFERS; i++) {
				g_buffers[i].inUse = true;
				MMRESULT result = waveOutWrite(g_hWaveOut, &g_buffers[i].header, sizeof(WAVEHDR));
				if (result != MMSYSERR_NOERROR) {
						printf("写入缓冲区 %d 失败! 错误代码: %d\n", i, result);
				}				
		}
    printf("开始播放随机噪声...\n");
}

// 停止播放
void StopPlayback() {
    if (!g_hWaveOut) return;
    
    g_bPlaying = false;
    waveOutReset(g_hWaveOut);  // 立即停止播放,触发所有缓冲区的 WOM_DONE 回调
    printf("停止播放\n");
}

// 清理资源
void CleanupAudio() {
    StopPlayback();
    
    if (g_hWaveOut) {
        // 取消准备所有缓冲区
        for (auto& buffer : g_buffers) {
            if (buffer.header.dwFlags & WHDR_PREPARED) {
                waveOutUnprepareHeader(g_hWaveOut, &buffer.header, sizeof(WAVEHDR));
            }
        }        
        waveOutClose(g_hWaveOut);
        g_hWaveOut = NULL;
    }
    g_buffers.clear();
    printf("音频资源已释放\n");
}

// 主函数
int main() {
    printf("Virtual Audio Cable 随机噪声播放器\n");
    printf("===================================\n");
    
    if (!InitializeAudio()) {
        printf("初始化失败,按任意键退出...\n");
        getchar();
        return 1;
    }
    bool running = true; 
		StartPlayback();
		int counter=30;
		while (--counter>0)
		{
				Sleep(1000);			
    }		
		StopPlayback();		
    CleanupAudio();    
    return 0;
}

理解要点 :这里生成的是白噪声,类似于电视无信号时的"雪花声"。每个采样点都是随机值,但受振幅限制。
回调机制解释 :想象一个工厂流水线,有4个工位(缓冲区)轮流工作。当一个工位完成工作(播放完音频),系统自动通知程序:"工位1已完成,可以准备下一批产品了"。程序收到通知后,立即为该工位准备新的音频数据,确保音频播放不间断。
缓冲区的作用

  • 避免卡顿:多个缓冲区轮流工作,一个播放时,其他可以准备数据
  • 平滑播放:100毫秒的缓冲区提供足够的时间处理数据
  • 降低延迟:合理的大小平衡了延迟和稳定性

三、编译与运行

1、编译命令

bash 复制代码
cl /EHsc /I. /Iinclude audio.cpp /link /LIBPATH:. winmm.lib
  • /EHsc:启用C++异常处理
  • /I. /Iinclude:包含当前目录和include目录的头文件
  • audio.cpp:源文件
  • /link /LIBPATH:. winmm.lib:链接Windows多媒体库

2、运行程序

编译后生成audio.exe,运行后可以看到:

bash 复制代码
C:\Users>audio.exe

输出

bash 复制代码
Virtual Audio Cable 随机噪声播放器
===================================
系统中有 4 个音频输出设备
设备 0: 设备 1: PHL 245E1 (HD Audio Driver for
设备 2: Line 1 (Virtual Audio Cable)
找到 Virtual Audio Cable 设备: Line 1 (Virtual Audio Cable) (索引: 2)
音频设备初始化成功
格式: 44100 Hz, 16 位, 2 声道
每个缓冲区大小: 17640 字节 (0.0 毫秒)
创建了 4 个音频缓冲区
开始播放随机噪声...

四、验证结果:使用VLC播放音频

  1. 打开VLC播放器,点击"媒体" → "打开捕获设备"
  2. 音频设备名称:选择"Line 1 (Virtual Audio Cable)"
  3. 设置
    • 音频采样率:44100 Hz
    • 音频声道:立体声
  4. 播放:点击播放,即可听到程序生成的白噪声
相关推荐
行稳方能走远5 分钟前
Android C++ 学习笔记2
c++
星火开发设计5 分钟前
链表详解及C++实现
数据结构·c++·学习·链表·指针·知识
修炼地6 分钟前
代码随想录算法训练营第五十三天 | 卡码网97. 小明逛公园(Floyd 算法)、卡码网127. 骑士的攻击(A * 算法)、最短路算法总结、图论总结
c++·算法·图论
QQ_4376643147 分钟前
Qt-框架
c++·qt
※※冰馨※※17 分钟前
【QT】初始化显示时正常,操作刷新后布局显示问题。
开发语言·c++·windows·qt
APIshop17 分钟前
Python 爬虫获取「item_video」——淘宝商品主图视频全流程拆解
爬虫·python·音视频
电脑小管家18 分钟前
DirectX报错怎么办?快速修复游戏和软件崩溃问题
windows·驱动开发·microsoft·计算机外设·电脑
天天睡大觉19 分钟前
Python学习6
windows·python·学习
溟洵19 分钟前
【C++ Qt 】中的多线程QThread已经线程安全相关的锁QMutex、QMutexLocker
c++·后端·qt
亮子AI19 分钟前
【Python】Typer应用如何打包为Windows下的.exe文件?
开发语言·windows·python