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

求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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 09:21:37