C#获取Action<> Lambda参数值与Unity+Photon Voice音频转WAV问题
问题1:从Lambda调用的Action<>中获取参数值并存入数组
针对这个问题,核心是利用C#的闭包特性,或者封装一个参数追踪类来实现,分两种常见场景给你说明:
场景1:在Lambda内部直接存储参数
如果只是想在Lambda执行时把参数存入数组/集合,直接在Lambda代码块里操作外部定义的存储容器就行:
// 定义一个集合来存储参数(用List比数组更灵活,后续可转数组) List<object> parameterStorage = new List<object>(); // 声明带参数的Action,比如Action<int, string> Action<int, string> myAction = (num, str) => { // 将参数添加到存储容器 parameterStorage.Add(num); parameterStorage.Add(str); // 这里可以保留你原本的Action逻辑 }; // 调用Action myAction(123, "Hello Lambda"); // 后续需要数组的话直接转换 object[] paramArray = parameterStorage.ToArray();
场景2:封装类统一追踪所有调用的参数
如果需要更规范地管理多次调用的参数,比如按调用顺序存储完整参数组,可以封装一个追踪类:
public class ActionParamTracker<T1, T2> { // 存储每次调用的参数组 public List<Tuple<T1, T2>> RecordedParams { get; } = new List<Tuple<T1, T2>>(); // 返回带追踪逻辑的Action public Action<T1, T2> TrackedAction => (p1, p2) => { RecordedParams.Add(Tuple.Create(p1, p2)); // 这里可以添加你原本要执行的业务逻辑 }; } // 使用示例 var tracker = new ActionParamTracker<int, string>(); Action<int, string> trackedAction = tracker.TrackedAction; // 多次调用Action trackedAction(456, "First Call"); trackedAction(789, "Second Call"); // 遍历查看所有记录的参数 foreach (var paramTuple in tracker.RecordedParams) { Debug.Log($"Param1: {paramTuple.Item1}, Param2: {paramTuple.Item2}"); }
问题2:Unity + Photon Voice 保存音频流为WAV文件
你已经能拿到音频帧的float数据了,接下来只需要三步:收集帧数据、转成WAV兼容的PCM格式、生成WAV文件头并写入磁盘,一步步拆解给你:
1. 先持续收集音频帧数据
首先定义类级别的容器来存储收到的音频帧,同时记录Photon Voice的音频参数(采样率、通道数要和Photon Voice的设置一致,一般语音是单声道、48kHz采样率):
// 存储所有收到的音频帧 private List<float> collectedAudioFrames = new List<float>(); // 音频参数,需和Photon Voice实际配置匹配 private int audioSampleRate = 48000; private int audioChannels = 1;
然后在你已有的OnAudioFrameAction回调里收集帧:
PhotonVoiceNetwork.Client.OnAudioFrameAction += (playerId, voiceId, frame) => { // 将当前帧的float数组添加到容器 collectedAudioFrames.AddRange(frame); Debug.LogFormat("***** {0} {1} {2}", playerId, voiceId, frame[0]); };
2. 将float音频数据转成WAV兼容的16位PCM字节
WAV文件的PCM数据通常是16位有符号整数,需要把Photon Voice返回的float(范围[-1,1])缩放到short(范围[-32768,32767])并转成字节:
private byte[] ConvertFloatsTo16BitPCM(List<float> floatSamples) { byte[] pcmBytes = new byte[floatSamples.Count * 2]; // 每个short占2字节 int byteIndex = 0; foreach (float sample in floatSamples) { // 把float缩放到short范围 short pcmSample = (short)(sample * short.MaxValue); // WAV使用小端字节序,拆分short为两个字节 pcmBytes[byteIndex++] = (byte)(pcmSample & 0xff); pcmBytes[byteIndex++] = (byte)((pcmSample >> 8) & 0xff); } return pcmBytes; }
3. 生成WAV文件头并写入完整文件
WAV有固定的文件头格式,我们构造头信息后,和PCM数据一起写入文件:
private void SaveAudioToWav(string savePath, byte[] pcmData, int sampleRate, int channels) { using (FileStream fileStream = new FileStream(savePath, FileMode.Create)) using (BinaryWriter writer = new BinaryWriter(fileStream)) { // 写入RIFF块 writer.Write(new char[] { 'R', 'I', 'F', 'F' }); int totalFileSize = 36 + pcmData.Length; writer.Write(totalFileSize); writer.Write(new char[] { 'W', 'A', 'V', 'E' }); // 写入fmt块(PCM格式) writer.Write(new char[] { 'f', 'm', 't', ' ' }); writer.Write(16); // PCM格式块大小 writer.Write((short)1); // PCM格式代码 writer.Write((short)channels); writer.Write(sampleRate); writer.Write(sampleRate * channels * 2); // 字节率:采样率*通道数*位深度/8(16位=2字节) writer.Write((short)(channels * 2)); // 块对齐:通道数*位深度/8 writer.Write((short)16); // 位深度 // 写入data块 writer.Write(new char[] { 'd', 'a', 't', 'a' }); writer.Write(pcmData.Length); writer.Write(pcmData); } }
4. 触发保存操作
比如加一个UI按钮,点击时执行保存:
public void TriggerSaveAudio() { if (collectedAudioFrames.Count == 0) { Debug.LogWarning("没有可保存的音频数据!"); return; } // 转换为16位PCM字节 byte[] pcmData = ConvertFloatsTo16BitPCM(collectedAudioFrames); // 保存路径用Unity的持久化路径,避免权限问题 string savePath = Path.Combine(Application.persistentDataPath, $"player_audio_{DateTime.Now.Ticks}.wav"); SaveAudioToWav(savePath, pcmData, audioSampleRate, audioChannels); Debug.Log($"音频已保存到:{savePath}"); // 可选:清空容器,准备下一次录制 collectedAudioFrames.Clear(); }
注意事项
- 务必确认Photon Voice的实际采样率,比如Opus解码后的采样率可能是48kHz或16kHz,参数不匹配会导致播放声音变调;
- 如果需要区分多个玩家的音频,可以用
Dictionary<int, List<float>>按playerId分别存储; - Unity中文件操作建议放到协程里异步执行,避免阻塞主线程影响帧率。
内容的提问来源于stack exchange,提问作者Towerss
相关产品推荐
相关产品推荐

