You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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与资源流的类型差异无关,所有异常均来自初始实现的逻辑错误:

  1. 核心调用缺失:生成流后未执行audio.Load(mStrm)就直接调用Play()。Android端播放器实现容错逻辑宽松,可在未显式Load的情况下尝试读取当前流内容,但UWP端实现严格要求必须先传入流完成加载才能触发播放,这是UWP无声的首要原因。
  2. 流生命周期错误:调用Play()后立刻执行writer.Close()、mStrm.Close()。UWP端音频播放采用异步流读取机制,播放启动时仅完成播放指令下发,实际音频数据需要在播放过程中从流中持续读取,提前关闭流会导致数据读取中断,无有效音频输出。
  3. WAV头计算错误:RIFF块的fileSize字段标准值为文件总长度减8字节(扣除RIFF标识、fileSize字段本身占用的8字节),初始实现的计算值多了4字节,导致播放器解析音频数据的偏移位置错误,读取到无效数据产生爆音。
  4. 波形截断冲激:正弦波首尾没有做平滑过渡,采样值从0直接跳变到正弦峰值、播放结束时从峰值直接跳回0,会产生明显的冲激爆音。
  5. 无效冗余调用: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:01:46