From f2222ab3f47c52963493918b02a57438e8b27e38 Mon Sep 17 00:00:00 2001 From: fgsfds Date: Tue, 3 Sep 2019 20:13:38 +0300 Subject: [PATCH] Sound: OpenAL: Add Opus support --- src/engine/e_soundfile_opus.pas | 176 ++++++++++++++++++ ...undfile_ogg.pas => e_soundfile_vorbis.pas} | 68 ++++--- src/game/Doom2DF.lpr | 10 +- src/lib/opus/opus.pas | 174 +++++++++++++++++ src/lib/vorbis/ogg.pas | 2 +- 5 files changed, 399 insertions(+), 31 deletions(-) create mode 100644 src/engine/e_soundfile_opus.pas rename src/engine/{e_soundfile_ogg.pas => e_soundfile_vorbis.pas} (79%) create mode 100644 src/lib/opus/opus.pas diff --git a/src/engine/e_soundfile_opus.pas b/src/engine/e_soundfile_opus.pas new file mode 100644 index 0000000..6a73609 --- /dev/null +++ b/src/engine/e_soundfile_opus.pas @@ -0,0 +1,176 @@ +(* Copyright (C) Doom 2D: Forever Developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *) +{$INCLUDE ../shared/a_modes.inc} +unit e_soundfile_opus; + +interface + +uses e_soundfile, opus, classes; + +type + // Opus loader + + TOpusLoader = class (TSoundLoader) + public + function Load(Data: Pointer; Len: LongWord; SStreaming: Boolean): Boolean; override; overload; + function Load(FName: string; SStreaming: Boolean): Boolean; override; overload; + function SetPosition(Pos: LongWord): Boolean; override; + function FillBuffer(Buf: Pointer; Len: LongWord): LongWord; override; + function GetAll(var OutPtr: Pointer): LongWord; override; + procedure Free(); override; + private + FOpus: POggOpusFile; + FBuf: Pointer; + end; + + TOpusLoaderFactory = class (TSoundLoaderFactory) + public + function MatchHeader(Data: Pointer; Len: LongWord): Boolean; override; + function MatchExtension(FName: string): Boolean; override; + function GetLoader(): TSoundLoader; override; + end; + +implementation + +uses sysutils, utils, e_log, xstreams, ogg, ctypes; + +function TOpusLoaderFactory.MatchHeader(Data: Pointer; Len: LongWord): Boolean; +const + OGG_HEADER = $5367674F; // 'OggS' +var + F: POggOpusFile = nil; +begin + Result := False; + + if Len < 27 then // header is at least 27 bytes + Exit; + if PLongWord(Data)^ <> OGG_HEADER then + Exit; + + // now we gotta check that this is indeed an opus file and not a vorbis file + + F := op_test_memory(Data, Len, nil); + Result := F <> nil; + if Result then op_free(F); +end; + +function TOpusLoaderFactory.MatchExtension(FName: string): Boolean; +begin + Result := GetFilenameExt(FName) = '.opus'; +end; + +function TOpusLoaderFactory.GetLoader(): TSoundLoader; +begin + Result := TOpusLoader.Create(); +end; + +(* TOpusLoader *) + +function TOpusLoader.Load(Data: Pointer; Len: LongWord; SStreaming: Boolean): Boolean; +begin + Result := False; + + FBuf := GetMem(Len); + if FBuf = nil then + begin + e_LogWriteln('Opus: Load(Data) failed: out of memory on copy'); + Exit; + end; + Move(Data^, FBuf^, Len); + + FOpus := op_open_memory(FBuf, Len, nil); + if FOpus = nil then + begin + Free(); + e_LogWriteln('Opus: Load(Data) failed: op_open_memory failed'); + Exit; + end; + + FFormat.Channels := 2; // we use ov_read_stereo + FFormat.SampleBits := 16; + FFormat.SampleRate := 48000; // is this even correct? + FStreaming := True; // opus is always streaming + + Result := True; +end; + +function TOpusLoader.Load(FName: string; SStreaming: Boolean): Boolean; +begin + Result := False; + + FOpus := op_open_file(PChar(FName), nil); + if FOpus = nil then + begin + e_LogWritefln('Opus: Load(%s) failed: op_open_file failed', [FName]); + Exit; + end; + + FFormat.Channels := 2; // we use ov_read_stereo + FFormat.SampleBits := 16; + FFormat.SampleRate := 48000; // is this even correct? + FStreaming := True; // opus is always streaming + + Result := True; +end; + +function TOpusLoader.SetPosition(Pos: LongWord): Boolean; +begin + Result := False; + if (FOpus = nil) or (op_seekable(FOpus) = 0) then Exit; + Result := op_pcm_seek(FOpus, Pos) = 0; +end; + +function TOpusLoader.FillBuffer(Buf: Pointer; Len: LongWord): LongWord; +var + Ret: cint; + Rx: Integer; +begin + Result := 0; + if FOpus = nil then Exit; + + Rx := 0; + + while Rx < Len do + begin + Ret := op_read_stereo(FOpus, Buf + Rx, (Len - Rx) div 2); + if Ret = OP_HOLE then continue; + if Ret < 0 then break; + if FLooping and (Ret = 0) then op_pcm_seek(FOpus, 0); // loop + Rx := Rx + Ret * 4; + end; + + Result := Rx; +end; + +function TOpusLoader.GetAll(var OutPtr: Pointer): LongWord; +begin + Result := 0; // always streaming +end; + +procedure TOpusLoader.Free(); +begin + if FOpus <> nil then + op_free(FOpus); + if FBuf <> nil then + FreeMem(FBuf); + FOpus := nil; + FBuf := nil; + FStreaming := False; +end; + +initialization + e_AddSoundLoader(TOpusLoaderFactory.Create()); +end. diff --git a/src/engine/e_soundfile_ogg.pas b/src/engine/e_soundfile_vorbis.pas similarity index 79% rename from src/engine/e_soundfile_ogg.pas rename to src/engine/e_soundfile_vorbis.pas index 220fbd2..1c237c1 100644 --- a/src/engine/e_soundfile_ogg.pas +++ b/src/engine/e_soundfile_vorbis.pas @@ -14,16 +14,16 @@ * along with this program. If not, see . *) {$INCLUDE ../shared/a_modes.inc} -unit e_soundfile_ogg; +unit e_soundfile_vorbis; interface uses e_soundfile, vorbis, classes; type - // OGG Vorbis loader + // Ogg Vorbis loader - TOGGLoader = class (TSoundLoader) + TVorbisLoader = class (TSoundLoader) public function Load(Data: Pointer; Len: LongWord; SStreaming: Boolean): Boolean; override; overload; function Load(FName: string; SStreaming: Boolean): Boolean; override; overload; @@ -43,7 +43,7 @@ type function LoadEntireStream(): Pointer; end; - TOGGLoaderFactory = class (TSoundLoaderFactory) + TVorbisLoaderFactory = class (TSoundLoaderFactory) public function MatchHeader(Data: Pointer; Len: LongWord): Boolean; override; function MatchExtension(FName: string): Boolean; override; @@ -107,33 +107,43 @@ var tell: streamTell; ); -(* TOGGLoaderFactory *) +(* TVorbisLoaderFactory *) -function TOGGLoaderFactory.MatchHeader(Data: Pointer; Len: LongWord): Boolean; +function TVorbisLoaderFactory.MatchHeader(Data: Pointer; Len: LongWord): Boolean; const OGG_HEADER = $5367674F; // 'OggS' +var + S: TStream; + F: OggVorbis_File; begin + Result := False; + if Len < 27 then // header is at least 27 bytes - begin - Result := False; - exit; - end; - Result := (PLongWord(Data)^ = OGG_HEADER); + Exit; + if PLongWord(Data)^ <> OGG_HEADER then + Exit; + + // now we gotta check that this is indeed a vorbis file and not an opus file + + S := TSFSMemoryStreamRO.Create(Data, Len); + Result := ov_test_callbacks(S, F, nil, 0, oggIO) = 0; + if Result then ov_clear(F); + S.Free(); end; -function TOGGLoaderFactory.MatchExtension(FName: string): Boolean; +function TVorbisLoaderFactory.MatchExtension(FName: string): Boolean; begin Result := GetFilenameExt(FName) = '.ogg'; end; -function TOGGLoaderFactory.GetLoader(): TSoundLoader; +function TVorbisLoaderFactory.GetLoader(): TSoundLoader; begin - Result := TOGGLoader.Create(); + Result := TVorbisLoader.Create(); end; -(* TOGGLoader *) +(* TVorbisLoader *) -function TOGGLoader.LoadEntireStream(): Pointer; +function TVorbisLoader.LoadEntireStream(): Pointer; var Samples: ogg_int64_t; Ret: clong; @@ -157,7 +167,7 @@ begin FTotal := Ret; end; -function TOGGLoader.LoadStream(Stream: TStream; SStreaming: Boolean): Boolean; +function TVorbisLoader.LoadStream(Stream: TStream; SStreaming: Boolean): Boolean; var Ret, Bit: clong; Info: pvorbis_info; @@ -197,7 +207,7 @@ begin end; ov_clear(FOgg); - Stream.Destroy(); + Stream.Free(); FreeMem(FBuf); FBuf := FullBuf; @@ -213,7 +223,7 @@ begin Result := True; end; -function TOGGLoader.Load(Data: Pointer; Len: LongWord; SStreaming: Boolean): Boolean; +function TVorbisLoader.Load(Data: Pointer; Len: LongWord; SStreaming: Boolean): Boolean; var S: TStream; begin @@ -230,13 +240,13 @@ begin if not Result and (S <> nil) then begin - S.Destroy(); + S.Free(); FreeMem(FBuf); FBuf := nil; end; end; -function TOGGLoader.Load(FName: string; SStreaming: Boolean): Boolean; +function TVorbisLoader.Load(FName: string; SStreaming: Boolean): Boolean; var S: TStream = nil; begin @@ -251,17 +261,17 @@ begin end; if not Result and (S <> nil) then - S.Destroy(); + S.Free(); end; -function TOGGLoader.SetPosition(Pos: LongWord): Boolean; +function TVorbisLoader.SetPosition(Pos: LongWord): Boolean; begin Result := False; if not FOpen or (ov_seekable(FOgg) = 0) then Exit; Result := ov_pcm_seek(FOgg, Pos) = 0; end; -function TOGGLoader.FillBuffer(Buf: Pointer; Len: LongWord): LongWord; +function TVorbisLoader.FillBuffer(Buf: Pointer; Len: LongWord): LongWord; var Ret: clong; begin @@ -269,10 +279,12 @@ begin if not FOpen or not FStreaming then Exit; Ret := ov_read_ext(FOgg, Buf, Len, False, 2, True); if Ret < 0 then Exit; + if FLooping and (Ret = 0) then + ov_pcm_seek(FOgg, 0); Result := Ret; end; -function TOGGLoader.GetAll(var OutPtr: Pointer): LongWord; +function TVorbisLoader.GetAll(var OutPtr: Pointer): LongWord; begin Result := 0; if FStreaming or (FTotal = 0) then Exit; @@ -280,12 +292,12 @@ begin OutPtr := FBuf; end; -procedure TOGGLoader.Free(); +procedure TVorbisLoader.Free(); begin if FOpen then ov_clear(FOgg); if FData <> nil then - FData.Destroy(); + FData.Free(); if FBuf <> nil then FreeMem(FBuf); FData := nil; @@ -296,5 +308,5 @@ begin end; initialization - e_AddSoundLoader(TOGGLoaderFactory.Create()); + e_AddSoundLoader(TVorbisLoaderFactory.Create()); end. diff --git a/src/game/Doom2DF.lpr b/src/game/Doom2DF.lpr index 0e7d218..834de4e 100644 --- a/src/game/Doom2DF.lpr +++ b/src/game/Doom2DF.lpr @@ -65,8 +65,7 @@ uses e_soundfile_wav in '../engine/e_soundfile_wav.pas', {$IFDEF USE_VORBIS} vorbis in '../lib/vorbis/vorbis.pas', - ogg in '../lib/vorbis/ogg.pas', - e_soundfile_ogg in '../engine/e_soundfile_ogg.pas', + e_soundfile_vorbis in '../engine/e_soundfile_vorbis.pas', {$ENDIF} {$IFDEF USE_FLUIDSYNTH} fluidsynth in '../lib/fluidsynth/fluidsynth.pas', @@ -84,6 +83,13 @@ uses mpg123 in '../lib/mpg123/mpg123.pas', e_soundfile_mp3 in '../engine/e_soundfile_mp3.pas', {$ENDIF} + {$IFDEF USE_OPUS} + opus in '../lib/opus/opus.pas', + e_soundfile_opus in '../engine/e_soundfile_opus.pas', + {$ENDIF} + {$IF DEFINED(USE_VORBIS) OR DEFINED(USE_OPUS)} + ogg in '../lib/vorbis/ogg.pas', // this has to come last because link order + {$ENDIF} {$ENDIF} ENet in '../lib/enet/enet.pp', e_graphics in '../engine/e_graphics.pas', diff --git a/src/lib/opus/opus.pas b/src/lib/opus/opus.pas new file mode 100644 index 0000000..b236e47 --- /dev/null +++ b/src/lib/opus/opus.pas @@ -0,0 +1,174 @@ +unit opus; + +{$MODE OBJFPC}{$H+} +{$PACKRECORDS C} +{$MINENUMSIZE 4} + +interface + +uses + CTypes, SysUtils, ogg; + +{$IF DEFINED(WINDOWS)} + {$IFDEF OPUS_WINDOZE_STATIC} + {$LINKLIB libopusfile.a} + {$LINKLIB libopus.a} + {$ELSE} + {$DEFINE OPUS_DYNAMIC} + const opuslib = 'libopus-0.dll'; + const opusfilelib = 'libopusfile-0.dll'; + {$ENDIF} +{$ELSEIF DEFINED(UNIX)} + {$DEFINE OPUS_DYNAMIC} + const opuslib = 'libopus.so'; + const opusfilelib = 'libopusfile.so'; +{$ELSE} + {$ERROR libopus not supported on this platform. Fix it!} +{$ENDIF} + +const + OP_FALSE = -1; + OP_HOLE = -3; + OP_EREAD = -128; + OP_EFAULT = -129; + OP_EIMPL = -130; + OP_EINVAL = -131; + OP_ENOTVORBIS = -132; + OP_EBADHEADER = -133; + OP_EVERSION = -134; + OP_ENOTAUDIO = -135; + OP_EBADPACKET = -136; + OP_EBADLINK = -137; + OP_ENOSEEK = -138; + OP_EBADTIMESTAMP = -139; + +const + OPUS_OK = 0; + OPUS_BAD_ARG = -1; + OPUS_BUFFER_TOO_SMALL = -2; + OPUS_INTERNAL_ERROR = -3; + OPUS_INVALID_PACKET = -4; + OPUS_UNIMPLEMENTED = -5; + OPUS_INVALID_STATE = -6; + OPUS_ALLOC_FAIL = -7; + + OPUS_APPLICATION_VOIP = 2048; + OPUS_APPLICATION_AUDIO = 2049; + OPUS_APPLICATION_RESTRICTED_LOWDELAY = 2051; + + OPUS_SIGNAL_VOICE = 3001; // Signal being encoded is voice + OPUS_SIGNAL_MUSIC = 3002; // Signal being encoded is music + + OPUS_BANDWIDTH_NARROWBAND = 1101; // 4 kHz bandpass @hideinitializer + OPUS_BANDWIDTH_MEDIUMBAND = 1102; // 6 kHz bandpass @hideinitializer + OPUS_BANDWIDTH_WIDEBAND = 1103; // 8 kHz bandpass @hideinitializer + OPUS_BANDWIDTH_SUPERWIDEBAND = 1104; // 12 kHz bandpass @hideinitializer + OPUS_BANDWIDTH_FULLBAND = 1105; // 20 kHz bandpass @hideinitializer + + OPUS_FRAMESIZE_ARG = 5000; // Select frame size from the argument (default) + OPUS_FRAMESIZE_2_5_MS = 5001; // Use 2.5 ms frames + OPUS_FRAMESIZE_5_MS = 5002; // Use 5 ms frames + OPUS_FRAMESIZE_10_MS = 5003; // Use 10 ms frames + OPUS_FRAMESIZE_20_MS = 5004; // Use 20 ms frames + OPUS_FRAMESIZE_40_MS = 5005; // Use 40 ms frames + OPUS_FRAMESIZE_60_MS = 5006; // Use 60 ms frames + + OPUS_CHANNEL_COUNT_MAX = 255; + +const + OPUS_SET_APPLICATION_REQUEST = 4000; + OPUS_GET_APPLICATION_REQUEST = 4001; + OPUS_SET_BITRATE_REQUEST = 4002; + OPUS_GET_BITRATE_REQUEST = 4003; + OPUS_SET_MAX_BANDWIDTH_REQUEST = 4004; + OPUS_GET_MAX_BANDWIDTH_REQUEST = 4005; + OPUS_SET_VBR_REQUEST = 4006; + OPUS_GET_VBR_REQUEST = 4007; + OPUS_SET_BANDWIDTH_REQUEST = 4008; + OPUS_GET_BANDWIDTH_REQUEST = 4009; + OPUS_SET_COMPLEXITY_REQUEST = 4010; + OPUS_GET_COMPLEXITY_REQUEST = 4011; + OPUS_SET_INBAND_FEC_REQUEST = 4012; + OPUS_GET_INBAND_FEC_REQUEST = 4013; + OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014; + OPUS_GET_PACKET_LOSS_PERC_REQUEST = 4015; + OPUS_SET_DTX_REQUEST = 4016; + OPUS_GET_DTX_REQUEST = 4017; + OPUS_SET_VBR_CONSTRAINT_REQUEST = 4020; + OPUS_GET_VBR_CONSTRAINT_REQUEST = 4021; + OPUS_SET_FORCE_CHANNELS_REQUEST = 4022; + OPUS_GET_FORCE_CHANNELS_REQUEST = 4023; + OPUS_SET_SIGNAL_REQUEST = 4024; + OPUS_GET_SIGNAL_REQUEST = 4025; + OPUS_GET_LOOKAHEAD_REQUEST = 4027; + OPUS_RESET_STATE_REQUEST = 4028; + OPUS_GET_SAMPLE_RATE_REQUEST = 4029; + OPUS_GET_FINAL_RANGE_REQUEST = 4031; + OPUS_GET_PITCH_REQUEST = 4033; + OPUS_SET_GAIN_REQUEST = 4034; + OPUS_GET_GAIN_REQUEST = 4045; + OPUS_SET_LSB_DEPTH_REQUEST = 4036; + OPUS_GET_LSB_DEPTH_REQUEST = 4037; + OPUS_GET_LAST_PACKET_DURATION_REQUEST = 4039; + OPUS_SET_EXPERT_FRAME_DURATION_REQUEST = 4040; + OPUS_GET_EXPERT_FRAME_DURATION_REQUEST = 4041; + OPUS_SET_PREDICTION_DISABLED_REQUEST = 4042; + OPUS_GET_PREDICTION_DISABLED_REQUEST = 4043; + OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST = 5120; + OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST = 5122; + +type + OpusHead = record + version: cint; + channel_count: cint; + pre_skip: cuint; + input_sample_rate: cuint32; + output_gain: cint; + mapping_family: cint; + stream_count: cint; + coupled_count: cint; + mapping: array [0..OPUS_CHANNEL_COUNT_MAX-1] of byte; + end; + POpusHead = ^OpusHead; + +type + OggOpusFile = record end; + POggOpusFile = ^OggOpusFile; + +type + op_read_func = function (stream: Pointer; buffer: pointer; nbytes: cint): cint; cdecl; + op_seek_func = function (stream: Pointer; offset: Int64; whence: cint): cint; cdecl; + op_tell_func = function (stream: Pointer): Int64; cdecl; + op_close_func = function (stream: Pointer): cint; cdecl; + + OpusFileCallbacks = record + read: op_read_func; + seek: op_seek_func; + tell: op_tell_func; + close: op_close_func; + end; + +function opus_get_version_string(): PAnsiChar; cdecl; external {$IFDEF OPUS_DYNAMIC}opuslib{$ENDIF}; +function opus_strerror(error: cint): PAnsiChar; cdecl; external {$IFDEF OPUS_DYNAMIC}opuslib{$ENDIF}; + +function op_open_file(path: pchar; err: pcint): POggOpusFile; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_open_memory(data: pointer; size: csize_t; err: pcint): POggOpusFile; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; + +function op_test_file(path: pchar; err: pcint): POggOpusFile; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_test_memory(data: pointer; size: csize_t; err: pcint): POggOpusFile; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; + +procedure op_free(opf: POggOpusFile); cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; + +function op_seekable(opf: POggOpusFile): cint; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_channel_count(opf: POggOpusFile): cuint32; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_head(opf: POggOpusFile; li: cint): POpusHead; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_pcm_tell(opf: POggOpusFile): cint64; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_pcm_total(opf: POggOpusFile; li: cint): cint64; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; + +function op_read(opf: POggOpusFile; pcm: pcint16; bufsiz: cint; li: cint): cint; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_read_stereo(opf: POggOpusFile; pcm: pcint16; bufsiz: cint): cint; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; +function op_pcm_seek(opf: POggOpusFile; pos: cint64): cint; cdecl; external {$IFDEF OPUS_DYNAMIC}opusfilelib{$ENDIF}; + +implementation + +end. diff --git a/src/lib/vorbis/ogg.pas b/src/lib/vorbis/ogg.pas index 118fbad..9ce03af 100644 --- a/src/lib/vorbis/ogg.pas +++ b/src/lib/vorbis/ogg.pas @@ -27,7 +27,7 @@ uses ctypes; {$IF DEFINED(WINDOWS)} - {$IFDEF VORBIS_WINDOZE_STATIC} + {$IF DEFINED(VORBIS_WINDOZE_STATIC) OR DEFINED(OPUS_WINDOZE_STATIC)} {$LINKLIB libogg.a} {$ELSE} {$DEFINE OGG_DYNAMIC} -- 2.29.2