求TMediaPlayer跨平台低开销快速音效替代方案(Delphi10.2安卓兼容问题)
解决Delphi 10.2 Android平台低开销WAV音效播放的onLoadComplete未触发问题
首先,咱们先明确核心问题:你提到的onLoadComplete事件未触发,大概率是因为Delphi 10.2对Android MediaPlayer的封装存在细节问题,或者你的音频加载流程不符合Android原生API的要求。结合你需要低开销的需求,我给你整理一套可行的实现方案:
一、排查核心原因
- 事件映射错误:Android原生MediaPlayer并没有
onLoadComplete事件,对应的加载完成回调是onPrepared。如果你的代码是封装了原生API,可能是映射时没正确绑定这个回调。 - 音频路径问题:Android下Assets目录的文件不能直接用本地路径访问,必须通过
AssetFileDescriptor加载,否则MediaPlayer无法正确读取文件,自然不会触发准备完成事件。 - 同步加载导致无回调:如果调用了
prepare()(同步加载),不会触发异步回调;必须调用prepareAsync()才会在加载完成后触发onPrepared。
二、低开销的实现代码(兼容Delphi 10.2 Android + Windows)
下面是一个复用MediaPlayer实例的低开销音效播放器类,解决Android端回调未触发的问题:
1. 核心播放器类
unit LowLatencySoundPlayer; interface uses System.SysUtils, System.Classes, {$IFDEF ANDROID} Androidapi.JNI.Media, Androidapi.JNI.JavaTypes, Androidapi.Helpers, {$ENDIF} {$IFDEF MSWINDOWS} MMSystem, {$ENDIF} System.Generics.Collections; type TSoundPlayer = class private {$IFDEF ANDROID} FMediaPlayer: JMediaPlayer; FAssetFD: JAssetFileDescriptor; procedure OnPrepared(Sender: TObject); procedure OnCompletion(Sender: TObject); class var FPlayerPool: TObjectPool<TSoundPlayer>; {$ENDIF} {$IFDEF MSWINDOWS} FWaveHandle: HMMIO; FWaveData: array of Byte; FWaveLength: Integer; {$ENDIF} FSoundPath: string; FIsPrepared: Boolean; procedure PrepareSound; public constructor Create(const ASoundPath: string); destructor Destroy; override; procedure Play; class function GetPlayer(const ASoundPath: string): TSoundPlayer; class procedure ReleasePlayer(Player: TSoundPlayer); end; implementation {$IFDEF ANDROID} procedure TSoundPlayer.OnPrepared(Sender: TObject); begin FIsPrepared := True; // 如果需要立即播放,可以在这里调用Play end; procedure TSoundPlayer.OnCompletion(Sender: TObject); begin FIsPrepared := False; FMediaPlayer.seekTo(0); // 重置到开头,方便下次播放 end; {$ENDIF} constructor TSoundPlayer.Create(const ASoundPath: string); begin inherited Create; FSoundPath := ASoundPath; FIsPrepared := False; PrepareSound; end; destructor TSoundPlayer.Destroy; begin {$IFDEF ANDROID} if Assigned(FMediaPlayer) then begin FMediaPlayer.release; FMediaPlayer := nil; end; if Assigned(FAssetFD) then begin FAssetFD.close; FAssetFD := nil; end; {$ENDIF} {$IFDEF MSWINDOWS} if FWaveHandle <> 0 then begin mmioClose(FWaveHandle, 0); FWaveHandle := 0; end; {$ENDIF} inherited; end; procedure TSoundPlayer.PrepareSound; {$IFDEF ANDROID} var LAssetManager: JAssetManager; {$ENDIF} {$IFDEF MSWINDOWS} var LMMIOInfo: TMMIOInfo; LWaveHeader: TWaveFormatEx; LChunkInfo: TMMCKInfo; LDataChunk: TMMCKInfo; {$ENDIF} begin {$IFDEF ANDROID} LAssetManager := TAndroidHelper.Context.getAssets; try FAssetFD := LAssetManager.openFd(StringToJString(FSoundPath)); FMediaPlayer := TJMediaPlayer.JavaClass.init; // 设置数据源 FMediaPlayer.setDataSource(FAssetFD.getFileDescriptor, FAssetFD.getStartOffset, FAssetFD.getLength); // 绑定回调 FMediaPlayer.setOnPreparedListener(TJMediaPlayer_OnPreparedListener.Wrap( procedure(mp: JMediaPlayer) begin OnPrepared(Self); end)); FMediaPlayer.setOnCompletionListener(TJMediaPlayer_OnCompletionListener.Wrap( procedure(mp: JMediaPlayer) begin OnCompletion(Self); end)); // 异步准备,触发onPrepared FMediaPlayer.prepareAsync; except on E: Exception do begin // 处理加载错误 FIsPrepared := False; end; end; {$ENDIF} {$IFDEF MSWINDOWS} FWaveHandle := mmioOpen(PChar(FSoundPath), nil, MMIO_READ or MMIO_ALLOCBUF); if FWaveHandle = 0 then Exit; ZeroMemory(@LMMIOInfo, SizeOf(LMMIOInfo)); LMMIOInfo.fccIOProc := mmioFOURCC('W', 'A', 'V', 'E'); mmioSetInfo(FWaveHandle, @LMMIOInfo, 0); // 读取RIFF块 ZeroMemory(@LChunkInfo, SizeOf(LChunkInfo)); LChunkInfo.ckid := mmioFOURCC('R', 'I', 'F', 'F'); if mmioDescend(FWaveHandle, @LChunkInfo, nil, MMIO_FINDRIFF) <> MMSYSERR_NOERROR then Exit; // 读取fmt块 ZeroMemory(@LDataChunk, SizeOf(LDataChunk)); LDataChunk.ckid := mmioFOURCC('f', 'm', 't', ' '); if mmioDescend(FWaveHandle, @LDataChunk, @LChunkInfo, MMIO_FINDCHUNK) <> MMSYSERR_NOERROR then Exit; mmioRead(FWaveHandle, @LWaveHeader, SizeOf(LWaveHeader)); mmioAscend(FWaveHandle, @LDataChunk, 0); // 读取data块 ZeroMemory(@LDataChunk, SizeOf(LDataChunk)); LDataChunk.ckid := mmioFOURCC('d', 'a', 't', 'a'); if mmioDescend(FWaveHandle, @LDataChunk, @LChunkInfo, MMIO_FINDCHUNK) <> MMSYSERR_NOERROR then Exit; FWaveLength := LDataChunk.cksize; SetLength(FWaveData, FWaveLength); mmioRead(FWaveHandle, @FWaveData[0], FWaveLength); FIsPrepared := True; {$ENDIF} end; procedure TSoundPlayer.Play; begin {$IFDEF ANDROID} if Assigned(FMediaPlayer) and FIsPrepared then begin FMediaPlayer.start; end else if not FIsPrepared then begin // 如果未准备完成,可以重新触发准备 PrepareSound; end; {$ENDIF} {$IFDEF MSWINDOWS} if FIsPrepared then begin sndPlaySound(@FWaveData[0], SND_MEMORY or SND_ASYNC or SND_NODEFAULT); end; {$ENDIF} end; {$IFDEF ANDROID} class function TSoundPlayer.GetPlayer(const ASoundPath: string): TSoundPlayer; begin if not Assigned(FPlayerPool) then FPlayerPool := TObjectPool<TSoundPlayer>.Create( function: TSoundPlayer begin Result := TSoundPlayer.Create(ASoundPath); end, procedure(AItem: TSoundPlayer) begin // 回收时重置播放器 if Assigned(AItem.FMediaPlayer) then AItem.FMediaPlayer.seekTo(0); end); Result := FPlayerPool.GetItem; end; class procedure TSoundPlayer.ReleasePlayer(Player: TSoundPlayer); begin if Assigned(FPlayerPool) then FPlayerPool.ReleaseItem(Player); end; {$ELSE} class function TSoundPlayer.GetPlayer(const ASoundPath: string): TSoundPlayer; begin Result := TSoundPlayer.Create(ASoundPath); end; class procedure TSoundPlayer.ReleasePlayer(Player: TSoundPlayer); begin Player.Free; end; {$ENDIF} end.
2. 使用示例
// 获取播放器实例(Android端复用,Windows端按需创建) var LPlayer: TSoundPlayer; begin LPlayer := TSoundPlayer.GetPlayer('sounds/click.wav'); // Assets下的路径 try LPlayer.Play; finally TSoundPlayer.ReleasePlayer(LPlayer); end; end;
三、关键优化点说明
- Android端回调修复:改用Android原生的
onPrepared回调,通过匿名方法正确绑定到Delphi的事件处理逻辑;使用prepareAsync()异步加载音频确保回调触发;正确处理Assets文件的加载,用AssetFileDescriptor获取数据源。 - 低开销设计:Android端使用对象池复用MediaPlayer实例,避免频繁创建/销毁的开销;Windows端直接读取WAV数据到内存,用
sndPlaySound播放,比TMediaPlayer开销小很多。 - 兼容性适配:通过条件编译分别处理Android和Windows平台的实现逻辑。
四、额外注意事项
- 确保Android项目中Assets目录存在,且WAV文件已正确添加到Assets中(在Delphi的Project Manager里右键Assets文件夹添加)
- WAV文件建议采用PCM格式,避免压缩格式带来的解码开销
- 如果需要同时播放多个音效,可以扩展对象池的容量,或者为不同音效创建独立的对象池
内容的提问来源于stack exchange,提问作者bLight
相关产品推荐
相关产品推荐

