From bd010cc3817ec66c9ceef4c95f067eff794a9d3a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:16:41 +0000
Subject: [PATCH 01/10] Initial plan
From 308071387b731a47c9a91ffd9fb9af9116c01ca7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:22:28 +0000
Subject: [PATCH 02/10] Fix workflow, obsolete API, null refs, resource leaks,
and locale-dependent parsing
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
.github/workflows/build.yml | 22 ++++++++++++++++------
AirPlay/AirPlay.csproj | 2 +-
AirPlay/AirPlayReceiver.cs | 15 ++++++++++-----
AirPlay/AirPlayService.cs | 5 ++++-
AirPlay/Listeners/AirTunesListener.cs | 20 +++++++++++++++-----
AirPlay/Listeners/StreamingListener.cs | 7 ++++---
AirPlay/Utils/Utilities.cs | 7 +++----
7 files changed, 53 insertions(+), 25 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fac76f9..0796724 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,7 +15,14 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- configuration: [Debug, Release]
+ configuration: [Release]
+ include:
+ - os: ubuntu-latest
+ rid: linux-x64
+ - os: windows-latest
+ rid: win-x64
+ - os: macos-latest
+ rid: osx-x64
steps:
- name: Checkout code
@@ -32,8 +39,11 @@ jobs:
- name: Build
run: dotnet build AirPlay.sln --configuration ${{ matrix.configuration }} --no-restore
- - name: Display build output location
- shell: bash
- run: |
- echo "Build completed for ${{ matrix.os }} - ${{ matrix.configuration }}"
- find AirPlay/bin/${{ matrix.configuration }} -type f -name "AirPlay.dll" || true
+ - name: Publish
+ run: dotnet publish AirPlay/AirPlay.csproj --configuration ${{ matrix.configuration }} --runtime ${{ matrix.rid }} --self-contained true --output ./publish/${{ matrix.rid }}
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: AirPlay-${{ matrix.rid }}
+ path: ./publish/${{ matrix.rid }}/
diff --git a/AirPlay/AirPlay.csproj b/AirPlay/AirPlay.csproj
index c0299f7..ab01dc3 100644
--- a/AirPlay/AirPlay.csproj
+++ b/AirPlay/AirPlay.csproj
@@ -3,7 +3,7 @@
Exe
net8.0
- $(NoWarn);SYSLIB0011
+ $(NoWarn);SYSLIB0011;SYSLIB0050
diff --git a/AirPlay/AirPlayReceiver.cs b/AirPlay/AirPlayReceiver.cs
index 3f9002f..6289e35 100644
--- a/AirPlay/AirPlayReceiver.cs
+++ b/AirPlay/AirPlayReceiver.cs
@@ -13,7 +13,7 @@
namespace AirPlay
{
- public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver
+ public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable
{
public event EventHandler OnSetVolumeReceived;
public event EventHandler OnH264DataReceived;
@@ -137,9 +137,14 @@ public void OnData(H264Data data)
OnH264DataReceived?.Invoke(this, data);
}
- public void OnPCMData(PcmData data)
- {
- OnPCMDataReceived?.Invoke(this, data);
- }
+ public void OnPCMData(PcmData data)
+ {
+ OnPCMDataReceived?.Invoke(this, data);
+ }
+
+ public void Dispose()
+ {
+ _mdns?.Stop();
+ }
}
}
\ No newline at end of file
diff --git a/AirPlay/AirPlayService.cs b/AirPlay/AirPlayService.cs
index df1c87a..395368d 100644
--- a/AirPlay/AirPlayService.cs
+++ b/AirPlay/AirPlayService.cs
@@ -100,7 +100,10 @@ public Task StopAsync(CancellationToken cancellationToken)
public void Dispose()
{
-
+ if (_airPlayReceiver is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
}
}
}
diff --git a/AirPlay/Listeners/AirTunesListener.cs b/AirPlay/Listeners/AirTunesListener.cs
index 96d7249..6a17b24 100644
--- a/AirPlay/Listeners/AirTunesListener.cs
+++ b/AirPlay/Listeners/AirTunesListener.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Plists;
@@ -9,7 +10,7 @@
using System.Threading.Tasks;
using AirPlay.DmapTagged;
using AirPlay.Models;
-using AirPlay.Models.Configs;
+using AirPlay.Models.Configs;
using AirPlay.Models.Enums;
using AirPlay.Services.Implementations;
using AirPlay.Utils;
@@ -485,7 +486,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
if (key.Equals("volume", StringComparison.OrdinalIgnoreCase))
{
// request.Body contains 'volume: N.NNNNNN'
- _receiver.OnSetVolume(decimal.Parse(val));
+ _receiver.OnSetVolume(decimal.Parse(val, CultureInfo.InvariantCulture));
}
else if (key.Equals("progress", StringComparison.OrdinalIgnoreCase))
{
@@ -541,7 +542,10 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
}
}
- await session.AudioControlListener.FlushAsync(next_seq);
+ if (session.AudioControlListener != null)
+ {
+ await session.AudioControlListener.FlushAsync(next_seq);
+ }
}
if (request.Type == RequestType.TEARDOWN)
{
@@ -560,13 +564,19 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
if (type == 110)
{
// Stop mirroring session
- await session.MirroringListener.StopAsync();
+ if (session.MirroringListener != null)
+ {
+ await session.MirroringListener.StopAsync();
+ }
}
// If audio session
if (type == 96)
{
// Stop audio session
- await session.AudioControlListener.StopAsync();
+ if (session.AudioControlListener != null)
+ {
+ await session.AudioControlListener.StopAsync();
+ }
}
}
}
diff --git a/AirPlay/Listeners/StreamingListener.cs b/AirPlay/Listeners/StreamingListener.cs
index d36bebe..45598b9 100644
--- a/AirPlay/Listeners/StreamingListener.cs
+++ b/AirPlay/Listeners/StreamingListener.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Collections.Generic;
-using System.IO;
+using System.Globalization;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
@@ -138,7 +139,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
return new KeyValuePair(splitted[0], splitted[1]);
}).ToDictionary(k => k.Key, v => v.Value);
- var startAt = dict.TryGetValue("Start-Position", out string dStartAt) ? decimal.Parse(dStartAt) : 0M;
+ var startAt = dict.TryGetValue("Start-Position", out string dStartAt) ? decimal.Parse(dStartAt, CultureInfo.InvariantCulture) : 0M;
var url = dict.TryGetValue("Content-Location", out string dUrl) ? dUrl : throw new ArgumentNullException(nameof(dUrl));
// DO SOMETHING HERE...
diff --git a/AirPlay/Utils/Utilities.cs b/AirPlay/Utils/Utilities.cs
index 335a70a..2e0e6a7 100644
--- a/AirPlay/Utils/Utilities.cs
+++ b/AirPlay/Utils/Utilities.cs
@@ -24,9 +24,8 @@ public static byte[] CopyOfRange(byte[] src, int start, int end)
public static byte[] Hash(byte[] first, byte[] last)
{
- var sha512 = new SHA512CryptoServiceProvider();
byte[] combined = first.Concat(last).ToArray();
- byte[] hashed = sha512.ComputeHash(combined);
+ byte[] hashed = SHA512.HashData(combined);
return hashed;
}
@@ -57,9 +56,9 @@ public static void Swap(byte[] arr, int idxA, int idxB)
public static byte[] WriteWavHeader(ushort numchannels, uint sampleRate, ushort bitsPerSample, uint tot)
{
- var stream = new MemoryStream();
+ using var stream = new MemoryStream();
+ using BinaryWriter bwl = new BinaryWriter(stream);
- BinaryWriter bwl = new BinaryWriter(stream);
bwl.Write(new char[4] { 'R', 'I', 'F', 'F' });
bwl.Write(tot + 38);
bwl.Write(new char[8] { 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ' });
From cc491a579ac8ae5aa2e66f4e3db7bf8c180c849a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:25:10 +0000
Subject: [PATCH 03/10] Simplify workflow to only build Windows (win-x64)
version
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
.github/workflows/build.yml | 21 +++++----------------
1 file changed, 5 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0796724..b7de20f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,20 +9,9 @@ on:
jobs:
build:
- runs-on: ${{ matrix.os }}
+ runs-on: windows-latest
permissions:
contents: read
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- configuration: [Release]
- include:
- - os: ubuntu-latest
- rid: linux-x64
- - os: windows-latest
- rid: win-x64
- - os: macos-latest
- rid: osx-x64
steps:
- name: Checkout code
@@ -37,13 +26,13 @@ jobs:
run: dotnet restore AirPlay.sln
- name: Build
- run: dotnet build AirPlay.sln --configuration ${{ matrix.configuration }} --no-restore
+ run: dotnet build AirPlay.sln --configuration Release --no-restore
- name: Publish
- run: dotnet publish AirPlay/AirPlay.csproj --configuration ${{ matrix.configuration }} --runtime ${{ matrix.rid }} --self-contained true --output ./publish/${{ matrix.rid }}
+ run: dotnet publish AirPlay/AirPlay.csproj --configuration Release --runtime win-x64 --self-contained true --output ./publish/win-x64
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
- name: AirPlay-${{ matrix.rid }}
- path: ./publish/${{ matrix.rid }}/
+ name: AirPlay-win-x64
+ path: ./publish/win-x64/
From a47b2096a0ebd5d6d093f75f5da4df317444879b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:32:20 +0000
Subject: [PATCH 04/10] Fix NETSDK1152 publish error: remove unused Curve25519
package and add ErrorOnDuplicatePublishOutputFiles
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/AirPlay.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/AirPlay/AirPlay.csproj b/AirPlay/AirPlay.csproj
index ab01dc3..9558c24 100644
--- a/AirPlay/AirPlay.csproj
+++ b/AirPlay/AirPlay.csproj
@@ -4,6 +4,7 @@
Exe
net8.0
$(NoWarn);SYSLIB0011;SYSLIB0050
+ false
@@ -24,7 +25,6 @@
-
From 985c708c96665e3a6fb88a6a524462796ecc08a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:45:48 +0000
Subject: [PATCH 05/10] Replace native codec libraries with managed NuGet
packages for audio decoding
- Replace native FDK-AAC (libfdk-aac-2.dll) with managed SharpJaad.AAC NuGet package
- Replace native libalac (libalac-0.dll) with managed LibALAC NuGet package
- Remove CodecLibrariesConfig and library path configuration (no longer needed)
- Remove CodecLibraries sections from all appsettings JSON files
- Update AACDecoder, ALACDecoder, AudioListener, AirTunesListener, AirPlayReceiver, Program.cs
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/AirPlay.csproj | 2 +
AirPlay/AirPlayReceiver.cs | 21 +-
.../Decoders/Implementations/AACDecoder.cs | 215 ++++++------------
.../Decoders/Implementations/ALACDecoder.cs | 69 ++----
AirPlay/Listeners/AirTunesListener.cs | 6 +-
AirPlay/Listeners/AudioListener.cs | 10 +-
AirPlay/Program.cs | 1 -
AirPlay/appsettings_linux.json | 4 -
AirPlay/appsettings_osx.json | 4 -
AirPlay/appsettings_win.json | 4 -
10 files changed, 99 insertions(+), 237 deletions(-)
diff --git a/AirPlay/AirPlay.csproj b/AirPlay/AirPlay.csproj
index 9558c24..c2186a9 100644
--- a/AirPlay/AirPlay.csproj
+++ b/AirPlay/AirPlay.csproj
@@ -17,6 +17,7 @@
+
@@ -25,6 +26,7 @@
+
diff --git a/AirPlay/AirPlayReceiver.cs b/AirPlay/AirPlayReceiver.cs
index 6289e35..85c274d 100644
--- a/AirPlay/AirPlayReceiver.cs
+++ b/AirPlay/AirPlayReceiver.cs
@@ -29,17 +29,16 @@ public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable
private readonly ushort _airPlayPort;
private readonly string _deviceId;
- public AirPlayReceiver(IOptions aprConfig, IOptions codecConfig, IOptions dumpConfig)
- {
- _airTunesPort = aprConfig?.Value?.AirTunesPort ?? 5000;
- _airPlayPort = aprConfig?.Value?.AirPlayPort ?? 7000;
- _deviceId = aprConfig?.Value?.DeviceMacAddress ?? "11:22:33:44:55:66";
- _instance = aprConfig?.Value?.Instance ?? throw new ArgumentNullException("apr.instance");
-
- var clConfig = codecConfig?.Value ?? throw new ArgumentNullException(nameof(codecConfig));
- var dConfig = dumpConfig?.Value ?? throw new ArgumentNullException(nameof(dumpConfig));
-
- _airTunesListener = new AirTunesListener(this, _airTunesPort, _airPlayPort, clConfig, dConfig);
+ public AirPlayReceiver(IOptions aprConfig, IOptions dumpConfig)
+ {
+ _airTunesPort = aprConfig?.Value?.AirTunesPort ?? 5000;
+ _airPlayPort = aprConfig?.Value?.AirPlayPort ?? 7000;
+ _deviceId = aprConfig?.Value?.DeviceMacAddress ?? "11:22:33:44:55:66";
+ _instance = aprConfig?.Value?.Instance ?? throw new ArgumentNullException("apr.instance");
+
+ var dConfig = dumpConfig?.Value ?? throw new ArgumentNullException(nameof(dumpConfig));
+
+ _airTunesListener = new AirTunesListener(this, _airTunesPort, _airPlayPort, dConfig);
}
public async Task StartListeners(CancellationToken cancellationToken)
diff --git a/AirPlay/Decoders/Implementations/AACDecoder.cs b/AirPlay/Decoders/Implementations/AACDecoder.cs
index e3de90d..5c79931 100644
--- a/AirPlay/Decoders/Implementations/AACDecoder.cs
+++ b/AirPlay/Decoders/Implementations/AACDecoder.cs
@@ -1,199 +1,114 @@
/*
- * I have mapped only used methods.
- * This code does not have all 'AAC Decoder' functionality
+ * AAC Decoder using SharpJaad.AAC managed library.
+ * No native library dependencies required.
*/
using System;
-using System.IO;
using System.Runtime.InteropServices;
using AirPlay.Models.Enums;
-using AirPlay.Utils;
+using SharpJaad.AAC;
namespace AirPlay
{
- public unsafe class AACDecoder : IDecoder, IDisposable
+ public class AACDecoder : IDecoder
{
- private IntPtr _handle;
- private IntPtr _decoder;
+ private Decoder _jaadDecoder;
+ private DecoderConfig _decoderConfig;
+ private SampleBuffer _outputBuffer = new SampleBuffer();
- private delegate IntPtr aacDecoder_Open(int transportFmt, uint nrOfLayers);
- private delegate AACDecoderError aacDecoder_ConfigRaw(IntPtr decoder, IntPtr[] conf, uint *length);
- private delegate AACDecoderError aacDecoder_Fill(IntPtr decoder, IntPtr[] pBuffer, uint *bufferSize, uint *pBytesValid);
- private delegate AACDecoderError aacDecoder_DecodeFrame(IntPtr decoder, IntPtr output, int pcm_pkt_size, uint flags);
- private delegate IntPtr aacDecoder_Close(IntPtr decoder);
+ private int _pcmPktSize;
- private aacDecoder_Open _aacDecoder_Open;
- private aacDecoder_ConfigRaw _aacDecoder_ConfigRaw;
- private aacDecoder_Fill _aacDecoder_Fill;
- private aacDecoder_DecodeFrame _aacDecoder_DecodeFrame;
- private aacDecoder_Close _aacDecoder_Close;
+ public AudioFormat Type => AudioFormat.AAC;
- private AudioObjectType _audioObjectType;
-
- public int _pcmPktSize;
-
- public AudioFormat Type => _audioObjectType == AudioObjectType.AOT_ER_AAC_ELD ? AudioFormat.AAC_ELD : AudioFormat.AAC;
-
- public AACDecoder(string libraryPath, TransportType transportFmt, AudioObjectType audioObjectType, uint nrOfLayers)
+ public AACDecoder(TransportType transportFmt, AudioObjectType audioObjectType, uint nrOfLayers)
{
- if (!File.Exists(libraryPath))
+ _decoderConfig = new DecoderConfig();
+ if (GetProfileFromAudioObjectType(audioObjectType) is Profile profile)
{
- throw new IOException("Library not found.");
+ _decoderConfig.SetProfile(profile);
+ }
+ else
+ {
+ throw new NotSupportedException($"AudioObjectType {audioObjectType} is not supported.");
}
-
- // Open library
- _handle = LibraryLoader.DlOpen(libraryPath, 0);
-
- // Get function pointers symbols
- IntPtr symAacDecoder_Open = LibraryLoader.DlSym(_handle, "aacDecoder_Open");
- IntPtr symAacDecoder_ConfigRaw = LibraryLoader.DlSym(_handle, "aacDecoder_ConfigRaw");
- IntPtr sysAacDecoder_GetStreamInfo = LibraryLoader.DlSym(_handle, "aacDecoder_GetStreamInfo");
- IntPtr sysAacDecoder_Fill = LibraryLoader.DlSym(_handle, "aacDecoder_Fill");
- IntPtr sysAacDecoder_DecodeFrame = LibraryLoader.DlSym(_handle, "aacDecoder_DecodeFrame");
- IntPtr sysAacDecoder_Close = LibraryLoader.DlSym(_handle, "aacDecoder_Close");
-
- // Get delegates for the function pointers
- _aacDecoder_Open = Marshal.GetDelegateForFunctionPointer(symAacDecoder_Open);
- _aacDecoder_ConfigRaw = Marshal.GetDelegateForFunctionPointer(symAacDecoder_ConfigRaw);
- _aacDecoder_Fill = Marshal.GetDelegateForFunctionPointer(sysAacDecoder_Fill);
- _aacDecoder_DecodeFrame = Marshal.GetDelegateForFunctionPointer(sysAacDecoder_DecodeFrame);
- _aacDecoder_Close = Marshal.GetDelegateForFunctionPointer(sysAacDecoder_Close);
-
- _decoder = _aacDecoder_Open((int)transportFmt, nrOfLayers);
-
- _audioObjectType = audioObjectType;
}
public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
{
_pcmPktSize = frameLength * channels * bitDepth / 8;
- var frequencyIndex = Enum.Parse($"F_{sampleRate}", true);
-
- var config = AudioSpecificConfig((int)_audioObjectType, (int)frequencyIndex, channels, bitDepth);
-
- return Config(config);
- }
-
- public int GetOutputStreamLength()
- {
- return _pcmPktSize;
- }
-
- public int DecodeFrame(byte[] input, ref byte[] output, int pcm_pkt_size)
- {
- AACDecoderError ret;
- uint pkt_size = (uint)input.Length;
- uint valid_size = (uint)input.Length;
- uint fdk_flags = 0;
-
- ret = Fill(_decoder, input, pkt_size, valid_size);
- if(ret != AACDecoderError.AAC_DEC_OK)
+ var jaadSampleRate = SampleFrequencyExtensions.FromFrequency(sampleRate);
+ _decoderConfig.SetSampleFrequency(jaadSampleRate);
+ if (GetChannelConfigurationFromChannelCount(channels) is ChannelConfiguration channelConfig)
{
- Console.WriteLine($"aacDecoder_Fill error: {ret}");
- return (int)ret;
+ _decoderConfig.SetChannelConfiguration(channelConfig);
}
-
- ret = InternalDecodeFrame(ref output, pcm_pkt_size, fdk_flags);
- if (ret != AACDecoderError.AAC_DEC_OK)
+ else
{
- Console.WriteLine($"aacDecoder_DecodeFrame error: {ret}");
- return (int)ret;
+ throw new NotSupportedException($"Channel count {channels} is not supported.");
}
- return (int)ret;
- }
-
- public void Dispose()
- {
- _aacDecoder_Close(_decoder);
- LibraryLoader.DlClose(_handle);
- Marshal.FreeBSTR(_handle);
+ _jaadDecoder = new Decoder(_decoderConfig);
+ return 0;
}
- private AACDecoderError Fill(IntPtr decoder, byte[] pBuffer, uint bufferSize, uint pBytesValid)
+ public int GetOutputStreamLength()
{
- var size = Marshal.SizeOf(pBuffer[0]) * pBuffer.Length;
- var ptr = Marshal.AllocHGlobal(size);
- Marshal.Copy(pBuffer, 0, ptr, pBuffer.Length);
-
- var byteArrayPtr = new IntPtr[]
- {
- ptr
- };
-
- var res = _aacDecoder_Fill(decoder, byteArrayPtr, &bufferSize, &pBytesValid);
-
- return res;
+ return _pcmPktSize;
}
- private AACDecoderError InternalDecodeFrame(ref byte[] output, int pcm_pkt_size, uint flags)
+ public int DecodeFrame(byte[] input, ref byte[] output, int length)
{
- int size = Marshal.SizeOf(output[0]) * output.Length;
- IntPtr ptr = Marshal.AllocHGlobal(size);
-
- AACDecoderError res;
- try
+ if (_jaadDecoder == null)
{
- res = _aacDecoder_DecodeFrame(_decoder, ptr, pcm_pkt_size, flags);
- if (res == AACDecoderError.AAC_DEC_OK)
- {
- Marshal.Copy(ptr, output, 0, pcm_pkt_size);
- }
+ throw new InvalidOperationException("Decoder is not configured. Call Config() before decoding.");
}
- finally
+ _jaadDecoder.DecodeFrame(input, _outputBuffer);
+ var data = _outputBuffer.Data;
+ if (data != null && data.Length > 0)
{
- if (ptr != IntPtr.Zero)
- Marshal.FreeHGlobal(ptr);
+ var copyLen = Math.Min(data.Length, output.Length);
+ Array.Copy(data, output, copyLen);
}
-
- return res;
+ return 0;
}
- private byte[] AudioSpecificConfig(int audioObjectType, int frequenceIndex, int channels, int bitDepth)
+ private static Profile? GetProfileFromAudioObjectType(AudioObjectType audioObjectType)
{
- string bin;
- if (audioObjectType >= 31)
+ return audioObjectType switch
{
- bin = Convert.ToString(31, 2).PadLeft(5, '0');
- bin += Convert.ToString(audioObjectType - 32, 2).PadLeft(6, '0');
- }
- else
- {
- bin = Convert.ToString(audioObjectType, 2).PadLeft(5, '0');
- }
-
- bin += Convert.ToString(frequenceIndex, 2).PadLeft(4, '0');
- bin += Convert.ToString(channels, 2).PadLeft(4, '0');
- bin += Convert.ToString(bitDepth, 2).PadLeft(5, '0');
- bin += "00000000";
-
- int nBytes = bin.Length / 8;
- byte[] bytes = new byte[nBytes];
- for (int i = 0; i < nBytes; i++)
- {
- bytes[i] = Convert.ToByte(bin.Substring(8 * i, 8), 2);
- }
-
- return bytes;
+ AudioObjectType.AOT_AAC_MAIN => Profile.AAC_MAIN,
+ AudioObjectType.AOT_AAC_LC => Profile.AAC_LC,
+ AudioObjectType.AOT_AAC_SSR => Profile.AAC_SSR,
+ AudioObjectType.AOT_AAC_LTP => Profile.AAC_LTP,
+ AudioObjectType.AOT_SBR => Profile.AAC_SBR,
+ AudioObjectType.AOT_AAC_SCAL => Profile.AAC_SCALABLE,
+ AudioObjectType.AOT_TWIN_VQ => Profile.TWIN_VQ,
+ AudioObjectType.AOT_ER_AAC_LC => Profile.ER_AAC_LC,
+ AudioObjectType.AOT_ER_AAC_LTP => Profile.ER_AAC_LTP,
+ AudioObjectType.AOT_ER_AAC_SCAL => Profile.ER_AAC_SCALABLE,
+ AudioObjectType.AOT_ER_TWIN_VQ => Profile.ER_TWIN_VQ,
+ AudioObjectType.AOT_ER_BSAC => Profile.ER_BSAC,
+ AudioObjectType.AOT_ER_AAC_LD => Profile.ER_AAC_LD,
+ _ => null
+ };
}
- private int Config(byte[] config)
+ private static ChannelConfiguration? GetChannelConfigurationFromChannelCount(int channels)
{
- uint length = (uint)config.Length;
-
- var size = Marshal.SizeOf(config[0]) * config.Length;
- var ptr = Marshal.AllocHGlobal(size);
- Marshal.Copy(config, 0, ptr, config.Length);
-
- var byteArrayPtr = new IntPtr[]
+ return channels switch
{
- ptr
+ 0 => ChannelConfiguration.CHANNEL_CONFIG_NONE,
+ 1 => ChannelConfiguration.CHANNEL_CONFIG_MONO,
+ 2 => ChannelConfiguration.CHANNEL_CONFIG_STEREO,
+ 3 => ChannelConfiguration.CHANNEL_CONFIG_STEREO_PLUS_CENTER,
+ 4 => ChannelConfiguration.CHANNEL_CONFIG_STEREO_PLUS_CENTER_PLUS_REAR_MONO,
+ 5 => ChannelConfiguration.CHANNEL_CONFIG_FIVE,
+ 6 => ChannelConfiguration.CHANNEL_CONFIG_FIVE_PLUS_ONE,
+ 7 => ChannelConfiguration.CHANNEL_CONFIG_SEVEN_PLUS_ONE,
+ 8 => ChannelConfiguration.CHANNEL_CONFIG_SEVEN_PLUS_ONE,
+ _ => null
};
-
- var res = _aacDecoder_ConfigRaw(_decoder, byteArrayPtr, &length);
-
- return (int)res;
}
}
diff --git a/AirPlay/Decoders/Implementations/ALACDecoder.cs b/AirPlay/Decoders/Implementations/ALACDecoder.cs
index 681fa83..3cb8f01 100644
--- a/AirPlay/Decoders/Implementations/ALACDecoder.cs
+++ b/AirPlay/Decoders/Implementations/ALACDecoder.cs
@@ -1,57 +1,35 @@
/*
- * I have mapped only used methods.
- * This code does not have all 'ALAC Decoder' functionality
+ * ALAC Decoder using LibALAC managed library.
+ * No native library dependencies required.
*/
using System;
-using System.IO;
-using System.Runtime.InteropServices;
using AirPlay.Models.Enums;
-using AirPlay.Utils;
namespace AirPlay
{
- public unsafe class ALACDecoder : IDecoder, IDisposable
+ public class ALACDecoder : IDecoder
{
- private IntPtr _handle;
- private IntPtr _decoder;
-
- private delegate IntPtr alacDecoder_InitializeDecoder(int sampleRate, int channels, int bitsPerSample, int framesPerPacket);
- private delegate int alacDecoder_DecodeFrame(IntPtr decoder, IntPtr inBuffer, IntPtr outBuffer, int* ioNumBytes);
-
- private alacDecoder_InitializeDecoder _alacDecoder_InitializeDecoder;
- private alacDecoder_DecodeFrame _alacDecoder_DecodeFrame;
-
private int _pcm_pkt_size = 0;
+ private LibALAC.Decoder _alacDecoder;
+
public AudioFormat Type => AudioFormat.ALAC;
- public ALACDecoder(string libraryPath)
+ public ALACDecoder()
{
- if (!File.Exists(libraryPath))
- {
- throw new IOException("Library not found.");
- }
-
- // Open library
- _handle = LibraryLoader.DlOpen(libraryPath, 0);
-
- // Get a function pointer symbol
- IntPtr symAlacDecoder_InitializeDecoder = LibraryLoader.DlSym(_handle, "InitializeDecoder");
- IntPtr symAlacDecoder_DecodeFrame = LibraryLoader.DlSym(_handle, "Decode");
-
- // Get a delegate for the function pointer
- _alacDecoder_InitializeDecoder = Marshal.GetDelegateForFunctionPointer(symAlacDecoder_InitializeDecoder);
- _alacDecoder_DecodeFrame = Marshal.GetDelegateForFunctionPointer(symAlacDecoder_DecodeFrame);
}
public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
{
_pcm_pkt_size = frameLength * channels * bitDepth / 8;
- _decoder = _alacDecoder_InitializeDecoder(sampleRate, channels, bitDepth, frameLength);
-
- return _decoder != IntPtr.Zero ? 0 : -1;
+ _alacDecoder = new LibALAC.Decoder(sampleRate, channels, bitDepth, frameLength);
+ if (_alacDecoder == null)
+ {
+ return -1;
+ }
+ return 0;
}
public int GetOutputStreamLength()
@@ -61,27 +39,12 @@ public int GetOutputStreamLength()
public int DecodeFrame(byte[] input, ref byte[] output, int outputLen)
{
- var size = Marshal.SizeOf(input[0]) * input.Length;
- var inputPtr = Marshal.AllocHGlobal(size);
- Marshal.Copy(input, 0, inputPtr, input.Length);
-
- var outSize = Marshal.SizeOf(output[0]) * output.Length;
- var outPtr = Marshal.AllocHGlobal(outSize);
-
- var res = _alacDecoder_DecodeFrame(_decoder, inputPtr, outPtr, &outputLen);
- if(res == 0)
+ if (_alacDecoder == null)
{
- Marshal.Copy(outPtr, output, 0, outputLen);
+ throw new InvalidOperationException("Decoder is not initialized. Call Config() first.");
}
-
- return res;
- }
-
- public void Dispose()
- {
- // Close the C++ library
- LibraryLoader.DlClose(_handle);
- Marshal.FreeBSTR(_handle);
+ output = _alacDecoder.Decode(input, input.Length);
+ return 0;
}
}
diff --git a/AirPlay/Listeners/AirTunesListener.cs b/AirPlay/Listeners/AirTunesListener.cs
index 6a17b24..e7fe363 100644
--- a/AirPlay/Listeners/AirTunesListener.cs
+++ b/AirPlay/Listeners/AirTunesListener.cs
@@ -33,15 +33,13 @@ public class AirTunesListener : BaseTcpListener
new byte[] { 0x46,0x50,0x4c,0x59,0x03,0x01,0x02,0x00,0x00,0x00,0x00,0x82,0x02,0x02,0xc1,0x69,0xa3,0x52,0xee,0xed,0x35,0xb1,0x8c,0xdd,0x9c,0x58,0xd6,0x4f,0x16,0xc1,0x51,0x9a,0x89,0xeb,0x53,0x17,0xbd,0x0d,0x43,0x36,0xcd,0x68,0xf6,0x38,0xff,0x9d,0x01,0x6a,0x5b,0x52,0xb7,0xfa,0x92,0x16,0xb2,0xb6,0x54,0x82,0xc7,0x84,0x44,0x11,0x81,0x21,0xa2,0xc7,0xfe,0xd8,0x3d,0xb7,0x11,0x9e,0x91,0x82,0xaa,0xd7,0xd1,0x8c,0x70,0x63,0xe2,0xa4,0x57,0x55,0x59,0x10,0xaf,0x9e,0x0e,0xfc,0x76,0x34,0x7d,0x16,0x40,0x43,0x80,0x7f,0x58,0x1e,0xe4,0xfb,0xe4,0x2c,0xa9,0xde,0xdc,0x1b,0x5e,0xb2,0xa3,0xaa,0x3d,0x2e,0xcd,0x59,0xe7,0xee,0xe7,0x0b,0x36,0x29,0xf2,0x2a,0xfd,0x16,0x1d,0x87,0x73,0x53,0xdd,0xb9,0x9a,0xdc,0x8e,0x07,0x00,0x6e,0x56,0xf8,0x50,0xce},
new byte[] { 0x46,0x50,0x4c,0x59,0x03,0x01,0x02,0x00,0x00,0x00,0x00,0x82,0x02,0x03,0x90,0x01,0xe1,0x72,0x7e,0x0f,0x57,0xf9,0xf5,0x88,0x0d,0xb1,0x04,0xa6,0x25,0x7a,0x23,0xf5,0xcf,0xff,0x1a,0xbb,0xe1,0xe9,0x30,0x45,0x25,0x1a,0xfb,0x97,0xeb,0x9f,0xc0,0x01,0x1e,0xbe,0x0f,0x3a,0x81,0xdf,0x5b,0x69,0x1d,0x76,0xac,0xb2,0xf7,0xa5,0xc7,0x08,0xe3,0xd3,0x28,0xf5,0x6b,0xb3,0x9d,0xbd,0xe5,0xf2,0x9c,0x8a,0x17,0xf4,0x81,0x48,0x7e,0x3a,0xe8,0x63,0xc6,0x78,0x32,0x54,0x22,0xe6,0xf7,0x8e,0x16,0x6d,0x18,0xaa,0x7f,0xd6,0x36,0x25,0x8b,0xce,0x28,0x72,0x6f,0x66,0x1f,0x73,0x88,0x93,0xce,0x44,0x31,0x1e,0x4b,0xe6,0xc0,0x53,0x51,0x93,0xe5,0xef,0x72,0xe8,0x68,0x62,0x33,0x72,0x9c,0x22,0x7d,0x82,0x0c,0x99,0x94,0x45,0xd8,0x92,0x46,0xc8,0xc3,0x59}
};
- private readonly CodecLibrariesConfig _codecConfig;
private readonly DumpConfig _dumpConfig;
- public AirTunesListener(IRtspReceiver receiver, ushort port, ushort airPlayPort, CodecLibrariesConfig codecConfig, DumpConfig dumpConfig) : base(port)
+ public AirTunesListener(IRtspReceiver receiver, ushort port, ushort airPlayPort, DumpConfig dumpConfig) : base(port)
{
_airTunesPort = port;
_airPlayPort = airPlayPort;
_receiver = receiver ?? throw new ArgumentNullException(nameof(receiver));
- _codecConfig = codecConfig ?? throw new ArgumentNullException(nameof(codecConfig));
_dumpConfig = dumpConfig ?? throw new ArgumentNullException(nameof(dumpConfig));
// First time that we instantiate AirPlayListener we must create a ED25519 KeyPair
@@ -445,7 +443,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
if (session.FairPlayReady && session.AudioSessionReady && session.AudioControlListener == null)
{
// Start 'AudioListener' (handle PCM/AAC/ALAC data received from iOS/macOS
- var control = new AudioListener(_receiver, session.SessionId, 7002, 7003, _codecConfig, _dumpConfig);
+ var control = new AudioListener(_receiver, session.SessionId, 7002, 7003, _dumpConfig);
await control.StartAsync(cancellationToken).ConfigureAwait(false);
session.AudioControlListener = control;
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index d3c1487..7107cc0 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -33,14 +33,12 @@ public class AudioListener : BaseUdpListener
private RaopBuffer _raopBuffer;
private Socket _cSocket;
- private readonly CodecLibrariesConfig _clConfig;
private readonly DumpConfig _dConfig;
- public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ushort dport, CodecLibrariesConfig clConfig, DumpConfig dConfig) : base(cport, dport)
+ public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ushort dport, DumpConfig dConfig) : base(cport, dport)
{
_receiver = receiver ?? throw new ArgumentNullException(nameof(receiver));
_sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
- _clConfig = clConfig ?? throw new ArgumentNullException(nameof(clConfig));
_dConfig = dConfig ?? throw new ArgumentNullException(nameof(dConfig));
_raopBuffer = RaopBufferInit();
@@ -490,7 +488,7 @@ private void InitializeDecoder (AudioFormat audioFormat)
var bitDepth = 16;
var sampleRate = 44100;
- _decoder = new ALACDecoder(_clConfig.ALACLibPath);
+ _decoder = new ALACDecoder();
_decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
}
else if (audioFormat == AudioFormat.AAC)
@@ -503,7 +501,7 @@ private void InitializeDecoder (AudioFormat audioFormat)
var bitDepth = 16;
var sampleRate = 44100;
- _decoder = new AACDecoder(_clConfig.AACLibPath, TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_MAIN, 1);
+ _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_MAIN, 1);
_decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
}
else if(audioFormat == AudioFormat.AAC_ELD)
@@ -516,7 +514,7 @@ private void InitializeDecoder (AudioFormat audioFormat)
var bitDepth = 16;
var sampleRate = 44100;
- _decoder = new AACDecoder(_clConfig.AACLibPath, TransportType.TT_MP4_RAW, AudioObjectType.AOT_ER_AAC_ELD, 1);
+ _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_ER_AAC_ELD, 1);
_decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
}
else
diff --git a/AirPlay/Program.cs b/AirPlay/Program.cs
index 6225118..5fedf66 100644
--- a/AirPlay/Program.cs
+++ b/AirPlay/Program.cs
@@ -61,7 +61,6 @@ public static async Task Main(string[] args)
services.AddOptions();
services.Configure(hostContext.Configuration.GetSection("AirPlayReceiver"));
- services.Configure(hostContext.Configuration.GetSection("CodecLibraries"));
services.Configure(hostContext.Configuration.GetSection("Dump"));
services.AddSingleton();
diff --git a/AirPlay/appsettings_linux.json b/AirPlay/appsettings_linux.json
index cb4d537..bf610ca 100644
--- a/AirPlay/appsettings_linux.json
+++ b/AirPlay/appsettings_linux.json
@@ -10,10 +10,6 @@
"AirPlayPort": 7000,
"DeviceMacAddress": "11:22:33:44:55:66"
},
- "CodecLibraries": {
- "AACLibPath": "/usr/local/lib/libfdk-aac.dylib",
- "ALACLibPath": "/usr/local/lib/libalac.dylib"
- },
"Dump": {
"Path": "/path_here/"
}
diff --git a/AirPlay/appsettings_osx.json b/AirPlay/appsettings_osx.json
index afd575a..90c53ae 100644
--- a/AirPlay/appsettings_osx.json
+++ b/AirPlay/appsettings_osx.json
@@ -10,10 +10,6 @@
"AirPlayPort": 7000,
"DeviceMacAddress": "10:20:30:40:50:60"
},
- "CodecLibraries": {
- "AACLibPath": "/usr/local/lib/libfdk-aac.dylib",
- "ALACLibPath": "/usr/local/lib/libalac.dylib"
- },
"Dump": {
"Path": "/Users/steebono/Desktop/dump/"
}
diff --git a/AirPlay/appsettings_win.json b/AirPlay/appsettings_win.json
index 90498fb..d7440da 100644
--- a/AirPlay/appsettings_win.json
+++ b/AirPlay/appsettings_win.json
@@ -10,10 +10,6 @@
"AirPlayPort": 7000,
"DeviceMacAddress": "11:22:33:44:55:66"
},
- "CodecLibraries": {
- "AACLibPath": "C:\\msys64\\home\\sbono\\fdk-aac-master\\.libs\\libfdk-aac-2.dll",
- "ALACLibPath": "C:\\msys64\\mingw64\\bin\\libalac-0.dll"
- },
"Dump": {
"Path": "C:\\Users\\Foo\\Desktop\\Dump\\"
}
From 01f6ccd14232f6235625a12383a41b2751c9f9b2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 21:48:26 +0000
Subject: [PATCH 06/10] Fix AAC_ELD type reporting and remove redundant null
check in ALACDecoder
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Decoders/Implementations/AACDecoder.cs | 5 ++++-
AirPlay/Decoders/Implementations/ALACDecoder.cs | 4 ----
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/AirPlay/Decoders/Implementations/AACDecoder.cs b/AirPlay/Decoders/Implementations/AACDecoder.cs
index 5c79931..f51fc64 100644
--- a/AirPlay/Decoders/Implementations/AACDecoder.cs
+++ b/AirPlay/Decoders/Implementations/AACDecoder.cs
@@ -17,11 +17,13 @@ public class AACDecoder : IDecoder
private SampleBuffer _outputBuffer = new SampleBuffer();
private int _pcmPktSize;
+ private AudioObjectType _audioObjectType;
- public AudioFormat Type => AudioFormat.AAC;
+ public AudioFormat Type => _audioObjectType == AudioObjectType.AOT_ER_AAC_ELD ? AudioFormat.AAC_ELD : AudioFormat.AAC;
public AACDecoder(TransportType transportFmt, AudioObjectType audioObjectType, uint nrOfLayers)
{
+ _audioObjectType = audioObjectType;
_decoderConfig = new DecoderConfig();
if (GetProfileFromAudioObjectType(audioObjectType) is Profile profile)
{
@@ -90,6 +92,7 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
AudioObjectType.AOT_ER_TWIN_VQ => Profile.ER_TWIN_VQ,
AudioObjectType.AOT_ER_BSAC => Profile.ER_BSAC,
AudioObjectType.AOT_ER_AAC_LD => Profile.ER_AAC_LD,
+ AudioObjectType.AOT_ER_AAC_ELD => Profile.ER_AAC_LD, // AAC-ELD mapped to closest supported profile
_ => null
};
}
diff --git a/AirPlay/Decoders/Implementations/ALACDecoder.cs b/AirPlay/Decoders/Implementations/ALACDecoder.cs
index 3cb8f01..fb8adcd 100644
--- a/AirPlay/Decoders/Implementations/ALACDecoder.cs
+++ b/AirPlay/Decoders/Implementations/ALACDecoder.cs
@@ -25,10 +25,6 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
_pcm_pkt_size = frameLength * channels * bitDepth / 8;
_alacDecoder = new LibALAC.Decoder(sampleRate, channels, bitDepth, frameLength);
- if (_alacDecoder == null)
- {
- return -1;
- }
return 0;
}
From 2c1e6a37de55ca950f0ad8b8182665a2b43b6693 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:07:16 +0000
Subject: [PATCH 07/10] Add NAudio-based audio playback, fix PCM buffer length
bug, add flush event
- Add NAudio (2.2.1) for Windows audio output via DirectSoundOut
- Create StreamingWaveProvider: thread-safe IWaveProvider buffering PCM data
- Create AudioOutputService: manages DirectSound with prebuffering and flush handling
- Fix critical bug: pcmData.Length was hardcoded to 960 instead of actual audiobuflen
- Add OnAudioFlush() to IRtspReceiver and OnAudioFlushReceived to IAirPlayReceiver
- Propagate RTSP FLUSH events from AudioListener to AirPlayReceiver
- Update AirPlayService to play PCM data through speakers and handle track changes
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/AirPlay.csproj | 1 +
AirPlay/AirPlayReceiver.cs | 10 +-
AirPlay/AirPlayService.cs | 69 ++++++-
AirPlay/IAirPlayReceiver.cs | 1 +
AirPlay/IRtspReceiver.cs | 1 +
AirPlay/Listeners/AudioListener.cs | 3 +-
AirPlay/Services/AudioOutputService.cs | 239 ++++++++++++++++++++++
AirPlay/Services/StreamingWaveProvider.cs | 100 +++++++++
8 files changed, 419 insertions(+), 5 deletions(-)
create mode 100644 AirPlay/Services/AudioOutputService.cs
create mode 100644 AirPlay/Services/StreamingWaveProvider.cs
diff --git a/AirPlay/AirPlay.csproj b/AirPlay/AirPlay.csproj
index c2186a9..7d8b8de 100644
--- a/AirPlay/AirPlay.csproj
+++ b/AirPlay/AirPlay.csproj
@@ -22,6 +22,7 @@
+
diff --git a/AirPlay/AirPlayReceiver.cs b/AirPlay/AirPlayReceiver.cs
index 85c274d..2fcb269 100644
--- a/AirPlay/AirPlayReceiver.cs
+++ b/AirPlay/AirPlayReceiver.cs
@@ -15,9 +15,10 @@ namespace AirPlay
{
public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable
{
- public event EventHandler OnSetVolumeReceived;
- public event EventHandler OnH264DataReceived;
+ public event EventHandler OnSetVolumeReceived;
+ public event EventHandler OnH264DataReceived;
public event EventHandler OnPCMDataReceived;
+ public event EventHandler OnAudioFlushReceived;
public const string AirPlayType = "_airplay._tcp";
public const string AirTunesType = "_raop._tcp";
@@ -141,6 +142,11 @@ public void OnPCMData(PcmData data)
OnPCMDataReceived?.Invoke(this, data);
}
+ public void OnAudioFlush()
+ {
+ OnAudioFlushReceived?.Invoke(this, EventArgs.Empty);
+ }
+
public void Dispose()
{
_mdns?.Stop();
diff --git a/AirPlay/AirPlayService.cs b/AirPlay/AirPlayService.cs
index 395368d..d180459 100644
--- a/AirPlay/AirPlayService.cs
+++ b/AirPlay/AirPlayService.cs
@@ -1,10 +1,12 @@
-using AirPlay.Models.Configs;
+using AirPlay.Models.Configs;
+using AirPlay.Services;
using AirPlay.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -15,7 +17,9 @@ public class AirPlayService : IHostedService, IDisposable
private readonly IAirPlayReceiver _airPlayReceiver;
private readonly DumpConfig _dConfig;
+ private AudioOutputService _audioOutput;
private List _audiobuf;
+ private readonly object _audioOutputLock = new object();
public AirPlayService(IAirPlayReceiver airPlayReceiver, IOptions dConfig)
{
@@ -23,6 +27,30 @@ public AirPlayService(IAirPlayReceiver airPlayReceiver, IOptions dCo
_dConfig = dConfig?.Value ?? throw new ArgumentNullException(nameof(dConfig));
}
+ private void RecreateAudioOutput()
+ {
+ lock (_audioOutputLock)
+ {
+ Console.WriteLine("Recreating audio output after unexpected stop...");
+
+ try
+ {
+ _audioOutput?.Dispose();
+ Thread.Sleep(200);
+
+ _audioOutput = new AudioOutputService();
+ _audioOutput.Initialize();
+ _audioOutput.PlaybackStoppedUnexpectedly += (s, e) => RecreateAudioOutput();
+
+ Console.WriteLine("Audio output recreated successfully");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to recreate audio output: {ex.Message}");
+ }
+ }
+ }
+
public async Task StartAsync(CancellationToken cancellationToken)
{
#if DUMP
@@ -49,6 +77,23 @@ public async Task StartAsync(CancellationToken cancellationToken)
}
#endif
+ // Initialize audio output on Windows
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ try
+ {
+ _audioOutput = new AudioOutputService();
+ _audioOutput.Initialize();
+ _audioOutput.PlaybackStoppedUnexpectedly += (s, e) => RecreateAudioOutput();
+ Console.WriteLine("Audio output initialized successfully");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to initialize audio output: {ex.Message}");
+ Console.WriteLine("Continuing without audio output...");
+ }
+ }
+
await _airPlayReceiver.StartListeners(cancellationToken);
await _airPlayReceiver.StartMdnsAsync().ConfigureAwait(false);
@@ -57,6 +102,15 @@ public async Task StartAsync(CancellationToken cancellationToken)
// SET VOLUME
};
+ _airPlayReceiver.OnAudioFlushReceived += (s, e) =>
+ {
+ Console.WriteLine("Audio flush received - restarting audio output for new track");
+ lock (_audioOutputLock)
+ {
+ _audioOutput?.HandleFlush();
+ }
+ };
+
// DUMP H264 VIDEO
_airPlayReceiver.OnH264DataReceived += (s, e) =>
{
@@ -72,7 +126,12 @@ public async Task StartAsync(CancellationToken cancellationToken)
_audiobuf = new List();
_airPlayReceiver.OnPCMDataReceived += (s, e) =>
{
- // DO SOMETHING WITH AUDIO DATA..
+ // Play audio through speakers
+ lock (_audioOutputLock)
+ {
+ _audioOutput?.AddSamples(e.Data, 0, e.Length);
+ }
+
#if DUMP
_audiobuf.AddRange(e.Data);
#endif
@@ -81,6 +140,9 @@ public async Task StartAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken)
{
+ _audioOutput?.Dispose();
+ _audioOutput = null;
+
#if DUMP
// DUMP WAV AUDIO
var bPath = _dConfig.Path;
@@ -100,6 +162,9 @@ public Task StopAsync(CancellationToken cancellationToken)
public void Dispose()
{
+ _audioOutput?.Dispose();
+ _audioOutput = null;
+
if (_airPlayReceiver is IDisposable disposable)
{
disposable.Dispose();
diff --git a/AirPlay/IAirPlayReceiver.cs b/AirPlay/IAirPlayReceiver.cs
index 3848e63..9f72683 100644
--- a/AirPlay/IAirPlayReceiver.cs
+++ b/AirPlay/IAirPlayReceiver.cs
@@ -10,6 +10,7 @@ public interface IAirPlayReceiver
event EventHandler OnSetVolumeReceived;
event EventHandler OnH264DataReceived;
event EventHandler OnPCMDataReceived;
+ event EventHandler OnAudioFlushReceived;
Task StartListeners(CancellationToken cancellationToken);
diff --git a/AirPlay/IRtspReceiver.cs b/AirPlay/IRtspReceiver.cs
index 4370da1..8097841 100644
--- a/AirPlay/IRtspReceiver.cs
+++ b/AirPlay/IRtspReceiver.cs
@@ -10,5 +10,6 @@ public interface IRtspReceiver
void OnSetVolume(decimal volume);
void OnData(H264Data data);
void OnPCMData(PcmData data);
+ void OnAudioFlush();
}
}
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index 7107cc0..8c4c85a 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -173,7 +173,7 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
{
var pcmData = new PcmData();
- pcmData.Length = 960;
+ pcmData.Length = audiobuflen;
pcmData.Data = audiobuf;
pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
@@ -198,6 +198,7 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
public Task FlushAsync(int nextSequence)
{
RaopBufferFlush(_raopBuffer, nextSequence);
+ _receiver.OnAudioFlush();
return Task.CompletedTask;
}
diff --git a/AirPlay/Services/AudioOutputService.cs b/AirPlay/Services/AudioOutputService.cs
new file mode 100644
index 0000000..bf5b700
--- /dev/null
+++ b/AirPlay/Services/AudioOutputService.cs
@@ -0,0 +1,239 @@
+using NAudio.Wave;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AirPlay.Services
+{
+ ///
+ /// Audio output service using NAudio for Windows audio playback.
+ /// Buffers decoded PCM data and plays through the default audio device.
+ ///
+ public class AudioOutputService : IDisposable
+ {
+ private IWavePlayer _waveOut;
+ private StreamingWaveProvider _streamProvider;
+ private readonly object _lock = new object();
+ private bool _disposed = false;
+ private int _sampleCount = 0;
+ private bool _isPlaying = false;
+ private bool _initialized = false;
+ private int _prebufferPackets;
+ private WaveFormat _waveFormat;
+ private bool _needsReinit = false;
+
+ public event EventHandler PlaybackStoppedUnexpectedly;
+
+ public void Initialize()
+ {
+ lock (_lock)
+ {
+ if (_disposed) return;
+
+ Console.WriteLine("Initializing audio output...");
+
+ // PCM format: 16-bit, 44.1kHz, stereo (as decoded by AAC-ELD/AAC/ALAC)
+ _waveFormat = new WaveFormat(44100, 16, 2);
+
+ // Create streaming provider that NAudio pulls from
+ _streamProvider = new StreamingWaveProvider(_waveFormat, maxQueueSize: 500);
+
+ // Create DirectSound device (but DON'T call Init() yet)
+ _waveOut = new DirectSoundOut(100); // 100ms latency
+
+ _waveOut.PlaybackStopped += OnPlaybackStopped;
+
+ // Don't call Init() until we have audio data in the queue.
+ // If Init() is called on an empty queue, DirectSound's render thread starts,
+ // gets silence, and may terminate.
+ _initialized = false;
+
+ // Prebuffer 50 packets (~1 second of audio) before calling Init()
+ _prebufferPackets = 50;
+
+ Console.WriteLine($"Audio device created: {_waveFormat.SampleRate}Hz, {_waveFormat.BitsPerSample}-bit, {_waveFormat.Channels} channels");
+ Console.WriteLine($" Will call Init() after prebuffering {_prebufferPackets} packets");
+ }
+ }
+
+ private void OnPlaybackStopped(object sender, StoppedEventArgs args)
+ {
+ if (_disposed) return;
+
+ if (args.Exception != null)
+ {
+ Console.WriteLine($"Playback stopped with error: {args.Exception.Message}");
+
+ lock (_lock)
+ {
+ _isPlaying = false;
+ _needsReinit = true;
+ }
+
+ PlaybackStoppedUnexpectedly?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Handle a track change (FLUSH event). Recreates the DirectSound device
+ /// for a clean state.
+ ///
+ public void HandleFlush()
+ {
+ lock (_lock)
+ {
+ if (_disposed) return;
+
+ Console.WriteLine("Track change detected - restarting audio output...");
+
+ try
+ {
+ if (_waveOut != null)
+ {
+ try
+ {
+ _waveOut.Stop();
+ _waveOut.Dispose();
+ }
+ catch { }
+ _waveOut = null;
+ }
+
+ _streamProvider?.Clear();
+
+ _initialized = false;
+ _isPlaying = false;
+ _sampleCount = 0;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error during flush cleanup: {ex.Message}");
+ }
+ }
+
+ // Wait outside the lock for COM cleanup
+ Thread.Sleep(200);
+
+ lock (_lock)
+ {
+ try
+ {
+ _waveOut = new DirectSoundOut(100);
+ _waveOut.PlaybackStopped += OnPlaybackStopped;
+ Console.WriteLine($"Audio device recreated for new track, waiting for {_prebufferPackets} packets before Init()...");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to recreate audio device: {ex.Message}");
+ _needsReinit = true;
+ }
+ }
+ }
+
+ ///
+ /// Add decoded PCM samples to the audio output queue.
+ ///
+ public void AddSamples(byte[] buffer, int offset, int count)
+ {
+ lock (_lock)
+ {
+ if (_disposed || _needsReinit) return;
+ if (_waveOut == null || _streamProvider == null) return;
+
+ try
+ {
+ if (buffer == null || count <= 0) return;
+ if (offset < 0 || offset + count > buffer.Length) return;
+
+ _streamProvider.AddSamples(buffer, offset, count);
+ _sampleCount++;
+
+ // Initialize DirectSound AFTER prebuffering
+ if (!_initialized && _sampleCount >= _prebufferPackets)
+ {
+ Console.WriteLine($"Prebuffered {_streamProvider.QueuedBuffers} packets, calling Init()...");
+ _waveOut.Init(_streamProvider);
+ _initialized = true;
+ }
+
+ // Start playback after Init
+ if (_initialized && !_isPlaying)
+ {
+ _waveOut.Play();
+ _isPlaying = true;
+ Console.WriteLine($"Audio playback started ({_streamProvider.QueuedBuffers} packets buffered)");
+ }
+
+ // Log status periodically
+ if (_sampleCount % 50000 == 0)
+ {
+ var state = _waveOut.PlaybackState;
+ var queued = _streamProvider.QueuedBuffers;
+ Console.WriteLine($"Audio: {_sampleCount} packets | Queue: {queued} | State: {state}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Audio error: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Recreate the audio output after an error.
+ ///
+ public void Recreate()
+ {
+ lock (_lock)
+ {
+ Console.WriteLine("Recreating audio output...");
+
+ try
+ {
+ _waveOut?.Dispose();
+ }
+ catch { }
+
+ Thread.Sleep(200);
+
+ try
+ {
+ _streamProvider?.Clear();
+ _waveOut = new DirectSoundOut(100);
+ _waveOut.PlaybackStopped += OnPlaybackStopped;
+ _initialized = false;
+ _isPlaying = false;
+ _sampleCount = 0;
+ _needsReinit = false;
+ Console.WriteLine("Audio output recreated successfully");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to recreate audio output: {ex.Message}");
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ _waveOut?.Stop();
+ _waveOut?.Dispose();
+ }
+ catch { }
+
+ _streamProvider?.Clear();
+ _waveOut = null;
+ _streamProvider = null;
+
+ Console.WriteLine($"Audio disposed (played {_sampleCount} packets total)");
+ }
+ }
+ }
+}
diff --git a/AirPlay/Services/StreamingWaveProvider.cs b/AirPlay/Services/StreamingWaveProvider.cs
new file mode 100644
index 0000000..4dc3d7c
--- /dev/null
+++ b/AirPlay/Services/StreamingWaveProvider.cs
@@ -0,0 +1,100 @@
+using NAudio.Wave;
+using System;
+using System.Collections.Concurrent;
+
+namespace AirPlay.Services
+{
+ ///
+ /// A thread-safe IWaveProvider that buffers PCM audio data in a queue.
+ /// NAudio's output device pulls data from this provider via Read().
+ ///
+ public class StreamingWaveProvider : IWaveProvider
+ {
+ private readonly ConcurrentQueue _queue = new ConcurrentQueue();
+ private readonly int _maxQueueSize;
+ private byte[] _currentBuffer;
+ private int _currentOffset;
+
+ public WaveFormat WaveFormat { get; }
+ public int QueuedBuffers => _queue.Count;
+ public long TotalBytesRead { get; private set; }
+ public DateTime LastReadTime { get; private set; } = DateTime.UtcNow;
+
+ public StreamingWaveProvider(WaveFormat waveFormat, int maxQueueSize = 500)
+ {
+ WaveFormat = waveFormat;
+ _maxQueueSize = maxQueueSize;
+ }
+
+ ///
+ /// Add PCM samples to the streaming queue.
+ /// Returns false if the queue is full and the sample was dropped.
+ ///
+ public bool AddSamples(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null || count <= 0)
+ return false;
+
+ if (_queue.Count >= _maxQueueSize)
+ return false;
+
+ var copy = new byte[count];
+ Array.Copy(buffer, offset, copy, 0, count);
+ _queue.Enqueue(copy);
+ return true;
+ }
+
+ ///
+ /// Clear the buffer queue (e.g., on track change).
+ ///
+ public void Clear()
+ {
+ while (_queue.TryDequeue(out _)) { }
+ _currentBuffer = null;
+ _currentOffset = 0;
+ }
+
+ ///
+ /// Called by NAudio's output device to pull audio data.
+ /// Returns silence (zeros) if no data is available.
+ ///
+ public int Read(byte[] buffer, int offset, int count)
+ {
+ LastReadTime = DateTime.UtcNow;
+ int bytesWritten = 0;
+
+ while (bytesWritten < count)
+ {
+ // If we have a partial buffer from previous read, use it
+ if (_currentBuffer != null && _currentOffset < _currentBuffer.Length)
+ {
+ int available = _currentBuffer.Length - _currentOffset;
+ int toCopy = Math.Min(available, count - bytesWritten);
+ Array.Copy(_currentBuffer, _currentOffset, buffer, offset + bytesWritten, toCopy);
+ _currentOffset += toCopy;
+ bytesWritten += toCopy;
+
+ if (_currentOffset >= _currentBuffer.Length)
+ {
+ _currentBuffer = null;
+ _currentOffset = 0;
+ }
+ }
+ else if (_queue.TryDequeue(out var next))
+ {
+ _currentBuffer = next;
+ _currentOffset = 0;
+ }
+ else
+ {
+ // No more data — fill remaining with silence
+ Array.Clear(buffer, offset + bytesWritten, count - bytesWritten);
+ bytesWritten = count;
+ }
+ }
+
+ TotalBytesRead += bytesWritten;
+ return bytesWritten;
+ }
+ }
+}
From bcec5900e4e3283a21fa1fea0711a37d5c087c96 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:10:03 +0000
Subject: [PATCH 08/10] Extract magic numbers to named constants in
AudioOutputService
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Services/AudioOutputService.cs | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/AirPlay/Services/AudioOutputService.cs b/AirPlay/Services/AudioOutputService.cs
index bf5b700..9dcd935 100644
--- a/AirPlay/Services/AudioOutputService.cs
+++ b/AirPlay/Services/AudioOutputService.cs
@@ -11,6 +11,10 @@ namespace AirPlay.Services
///
public class AudioOutputService : IDisposable
{
+ private const int COM_CLEANUP_DELAY_MS = 200;
+ private const int PREBUFFER_PACKETS = 50;
+ private const int LOG_FREQUENCY_PACKETS = 50000;
+
private IWavePlayer _waveOut;
private StreamingWaveProvider _streamProvider;
private readonly object _lock = new object();
@@ -18,7 +22,6 @@ public class AudioOutputService : IDisposable
private int _sampleCount = 0;
private bool _isPlaying = false;
private bool _initialized = false;
- private int _prebufferPackets;
private WaveFormat _waveFormat;
private bool _needsReinit = false;
@@ -48,11 +51,8 @@ public void Initialize()
// gets silence, and may terminate.
_initialized = false;
- // Prebuffer 50 packets (~1 second of audio) before calling Init()
- _prebufferPackets = 50;
-
Console.WriteLine($"Audio device created: {_waveFormat.SampleRate}Hz, {_waveFormat.BitsPerSample}-bit, {_waveFormat.Channels} channels");
- Console.WriteLine($" Will call Init() after prebuffering {_prebufferPackets} packets");
+ Console.WriteLine($" Will call Init() after prebuffering {PREBUFFER_PACKETS} packets");
}
}
@@ -112,7 +112,7 @@ public void HandleFlush()
}
// Wait outside the lock for COM cleanup
- Thread.Sleep(200);
+ Thread.Sleep(COM_CLEANUP_DELAY_MS);
lock (_lock)
{
@@ -120,7 +120,7 @@ public void HandleFlush()
{
_waveOut = new DirectSoundOut(100);
_waveOut.PlaybackStopped += OnPlaybackStopped;
- Console.WriteLine($"Audio device recreated for new track, waiting for {_prebufferPackets} packets before Init()...");
+ Console.WriteLine($"Audio device recreated for new track, waiting for {PREBUFFER_PACKETS} packets before Init()...");
}
catch (Exception ex)
{
@@ -149,7 +149,7 @@ public void AddSamples(byte[] buffer, int offset, int count)
_sampleCount++;
// Initialize DirectSound AFTER prebuffering
- if (!_initialized && _sampleCount >= _prebufferPackets)
+ if (!_initialized && _sampleCount >= PREBUFFER_PACKETS)
{
Console.WriteLine($"Prebuffered {_streamProvider.QueuedBuffers} packets, calling Init()...");
_waveOut.Init(_streamProvider);
@@ -165,7 +165,7 @@ public void AddSamples(byte[] buffer, int offset, int count)
}
// Log status periodically
- if (_sampleCount % 50000 == 0)
+ if (_sampleCount % LOG_FREQUENCY_PACKETS == 0)
{
var state = _waveOut.PlaybackState;
var queued = _streamProvider.QueuedBuffers;
@@ -194,7 +194,7 @@ public void Recreate()
}
catch { }
- Thread.Sleep(200);
+ Thread.Sleep(COM_CLEANUP_DELAY_MS);
try
{
From 51f04901cc4016d229651554ca859098f3346b50 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:26:51 +0000
Subject: [PATCH 09/10] Fix infinite dequeue loop causing audio flooding and
silence
- Fix RaopBufferDequeue: return null instead of silent buffer when entry
not available (was causing infinite loop without advancing FirstSeqNum)
- Fix RaopBufferDequeue: advance FirstSeqNum on buffer overrun skip
- Add dequeue rate limiting: max 10 frames per received packet
- Skip empty/zero-length decoded frames in dequeue loop
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Listeners/AudioListener.cs | 35 +++++++++++++++---------------
1 file changed, 17 insertions(+), 18 deletions(-)
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index 8c4c85a..e7da7a1 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -169,9 +169,15 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
//if(_raopBuffer.LastSeqNum - _raopBuffer.FirstSeqNum > (RAOP_BUFFER_LENGTH / 8))
//{
- // Dequeue all frames in queue
- while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
+ // Dequeue frames from buffer (limit per packet to prevent flooding)
+ int dequeueCount = 0;
+ const int maxDequeuePerPacket = 10;
+ while (dequeueCount < maxDequeuePerPacket &&
+ (audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
{
+ if (audiobuf.Length == 0 || audiobuflen <= 0)
+ continue;
+
var pcmData = new PcmData();
pcmData.Length = audiobuflen;
pcmData.Data = audiobuf;
@@ -179,6 +185,7 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
_receiver.OnPCMData(pcmData);
+ dequeueCount++;
}
//}
@@ -373,25 +380,17 @@ public byte[] RaopBufferDequeue(RaopBuffer raop_buffer, ref int length, ref uint
/* Check how much we have space left in the buffer */
if (buflen < RAOP_BUFFER_LENGTH)
{
- /* Return nothing and hope resend gets on time */
- length = entry.AudioBufferSize;
- Array.Fill(entry.AudioBuffer, 0, 0, length);
-
- return entry.AudioBuffer;
+ /* Entry not yet available, wait for resend - return null to stop dequeue loop */
+ return null;
}
- /* Risk of buffer overrun, return empty buffer */
- return Array.Empty();
- }
- /* Update buffer and validate entry */
- if (!entry.Available)
- {
- /* Return an empty audio buffer to skip audio */
- length = entry.AudioBufferSize;
- Array.Fill(entry.AudioBuffer, 0, 0, length);
-
- return entry.AudioBuffer;
+ /* Risk of buffer overrun, skip this entry and advance */
+ entry.AudioBufferLen = 0;
+ raop_buffer.Entries[raop_buffer.FirstSeqNum % RAOP_BUFFER_LENGTH] = entry;
+ raop_buffer.FirstSeqNum += 1;
+ return null;
}
+
entry.Available = false;
/* Return entry audio buffer */
From f1f3697e5e90b9ea9f539ab7b3877bbb2f7a399b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Feb 2026 22:29:09 +0000
Subject: [PATCH 10/10] Address code review: move dequeueCount increment before
skip, add comments
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Listeners/AudioListener.cs | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index e7da7a1..6eb8e78 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -169,12 +169,14 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
//if(_raopBuffer.LastSeqNum - _raopBuffer.FirstSeqNum > (RAOP_BUFFER_LENGTH / 8))
//{
- // Dequeue frames from buffer (limit per packet to prevent flooding)
+ // Dequeue frames from buffer, limit to prevent flooding the output queue
int dequeueCount = 0;
const int maxDequeuePerPacket = 10;
while (dequeueCount < maxDequeuePerPacket &&
(audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
{
+ dequeueCount++;
+
if (audiobuf.Length == 0 || audiobuflen <= 0)
continue;
@@ -185,7 +187,6 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
_receiver.OnPCMData(pcmData);
- dequeueCount++;
}
//}
@@ -384,7 +385,7 @@ public byte[] RaopBufferDequeue(RaopBuffer raop_buffer, ref int length, ref uint
return null;
}
- /* Risk of buffer overrun, skip this entry and advance */
+ /* Risk of buffer overrun, skip this entry and advance to prevent getting stuck */
entry.AudioBufferLen = 0;
raop_buffer.Entries[raop_buffer.FirstSeqNum % RAOP_BUFFER_LENGTH] = entry;
raop_buffer.FirstSeqNum += 1;