Xamarin.UWP中Xam.Plugin.SimpleAudioPlayer无法播放内存生成音频流
Xamarin.Forms 自生成正弦波音频播放问题修复方案
问题场景
开发面向Windows与移动端的Xamarin.Forms跨平台应用时,自行生成正弦波WAV格式MemoryStream,借助Xam.Plugin.SimpleAudioPlayer实现播放,初始实现代码如下:
var mStrm = new MemoryStream(); BinaryWriter writer = new BinaryWriter(mStrm); const double TAU = 2 * Math.PI; int formatChunkSize = 16; int headerSize = 8; short formatType = 1; short tracks = 1; int samplesPerSecond = 44100; short bitsPerSample = 16; short frameSize = (short)(tracks * ((bitsPerSample + 7) / 8)); int bytesPerSecond = samplesPerSecond * frameSize; int waveSize = 4; int samples = (int)((decimal)samplesPerSecond * msDuration / 1000); int dataChunkSize = samples * frameSize; int fileSize = waveSize + headerSize + formatChunkSize + headerSize + dataChunkSize; writer.Write(0x46464952); // RIFF标识 writer.Write(fileSize); writer.Write(0x45564157); // WAVE标识 writer.Write(0x20746D66); // fmt 块标识 writer.Write(formatChunkSize); writer.Write(formatType); writer.Write(tracks); writer.Write(samplesPerSecond); writer.Write(bytesPerSecond); writer.Write(frameSize); writer.Write(bitsPerSample); writer.Write(0x61746164); // data块标识 writer.Write(dataChunkSize); { double theta = frequency * TAU / (double)samplesPerSecond; double amp = volume >> 2; // 振幅按比例缩放 for (int step = 0; step < samples; step++) { short s = (short)(amp * Math.Sin(theta * (double)step)); writer.Write(s); } } mStrm.ToArray(); mStrm.Seek(0, SeekOrigin.Begin); var audio = CrossSimpleAudioPlayer.Current; audio.Play(); writer.Close(); mStrm.Close();
该实现存在两个明确问题:
- Android模拟器上可基本运行,但播放时存在音频爆音
- UWP平台部署后完全无声音输出
作为对照,读取程序集嵌入WAV资源流播放的实现可在双端正常运行,获取资源流代码如下:
static Stream GetStreamFromFile(string filename) { var assembly = typeof(App).GetTypeInfo().Assembly; var stream = assembly.GetManifestResourceStream("SignalApplication." + filename); return stream; }
播放调用代码如下:
var stream1 = GetStreamFromFile("sinePeriodical.wav"); var audio = CrossSimpleAudioPlayer.Current; stream1.Seek(0L, SeekOrigin.Begin); audio.Load(stream1); audio.Play();
问题根因
问题和MemoryStream与资源流的类型差异无关,所有异常均来自初始实现的逻辑错误:
- 核心调用缺失:生成流后未执行
audio.Load(mStrm)就直接调用Play()。Android端播放器实现容错逻辑宽松,可在未显式Load的情况下尝试读取当前流内容,但UWP端实现严格要求必须先传入流完成加载才能触发播放,这是UWP无声的首要原因。 - 流生命周期错误:调用
Play()后立刻执行writer.Close()、mStrm.Close()。UWP端音频播放采用异步流读取机制,播放启动时仅完成播放指令下发,实际音频数据需要在播放过程中从流中持续读取,提前关闭流会导致数据读取中断,无有效音频输出。 - WAV头计算错误:RIFF块的
fileSize字段标准值为文件总长度减8字节(扣除RIFF标识、fileSize字段本身占用的8字节),初始实现的计算值多了4字节,导致播放器解析音频数据的偏移位置错误,读取到无效数据产生爆音。 - 波形截断冲激:正弦波首尾没有做平滑过渡,采样值从0直接跳变到正弦峰值、播放结束时从峰值直接跳回0,会产生明显的冲激爆音。
- 无效冗余调用:
mStrm.ToArray()未接收返回值,属于无意义操作,不影响流状态。
修复方案
修正后的正弦波流生成代码
public Stream GenerateSineWaveStream(int frequency, int msDuration, int volume = 16383) { var mStrm = new MemoryStream(); // leaveOpen设置为true,避免writer关闭时连带释放MemoryStream using var writer = new BinaryWriter(mStrm, Encoding.UTF8, leaveOpen: true); const double TAU = 2 * Math.PI; const int formatChunkSize = 16; const short formatType = 1; const short tracks = 1; const int samplesPerSecond = 44100; const short bitsPerSample = 16; const short frameSize = tracks * (bitsPerSample / 8); const int bytesPerSecond = samplesPerSecond * frameSize; int samples = (int)((decimal)samplesPerSecond * msDuration / 1000); int dataChunkSize = samples * frameSize; // 修正RIFF块大小计算 int fileSize = 36 + dataChunkSize; // 写入标准WAV文件头 writer.Write(0x46464952); // "RIFF" writer.Write(fileSize); writer.Write(0x45564157); // "WAVE" writer.Write(0x20746D66); // "fmt " writer.Write(formatChunkSize); writer.Write(formatType); writer.Write(tracks); writer.Write(samplesPerSecond); writer.Write(bytesPerSecond); writer.Write(frameSize); writer.Write(bitsPerSample); writer.Write(0x61746164); // "data" writer.Write(dataChunkSize); // 写入采样数据,增加首尾淡入淡出消除截断爆音 double theta = frequency * TAU / samplesPerSecond; double amp = Math.Clamp(volume, 0, short.MaxValue); // 取总时长1/10、最长441个采样点(约10ms)作为淡入淡出区间 int fadeSamples = Math.Min(samples / 10, 441); for (int step = 0; step < samples; step++) { double currentAmp = amp; // 淡入逻辑 if (step < fadeSamples) { currentAmp *= (double)step / fadeSamples; } // 淡出逻辑 else if (step > samples - fadeSamples) { currentAmp *= (double)(samples - step) / fadeSamples; } short sampleValue = (short)(currentAmp * Math.Sin(theta * step)); writer.Write(sampleValue); } writer.Flush(); mStrm.Position = 0; return mStrm; }
正确的播放调用逻辑
// 生成指定频率、时长的正弦波流 var audioStream = GenerateSineWaveStream(frequency: 440, msDuration: 2000); var audioPlayer = CrossSimpleAudioPlayer.Current; // 必须先加载流再触发播放 audioPlayer.Load(audioStream); audioPlayer.Play(); // 播放完成后再释放流资源,避免提前关闭导致读取失败 audioPlayer.PlaybackEnded += (sender, args) => { audioStream.Dispose(); };
注意事项
- 不要在播放启动后立刻释放流,UWP端异步读取机制要求流在播放过程中保持可用。
- 淡入淡出区间长度可根据实际听感调整,可完全消除波形跳变产生的爆音。
- 只要流的Position重置到0、内容符合WAV格式标准,
MemoryStream和嵌入资源返回的流在播放器中表现完全一致,不存在类型兼容问题。
内容的提问来源于stack exchange,提问作者Yawar Khan
相关产品推荐
相关产品推荐

