1 | unit Sound;
|
---|
2 |
|
---|
3 | interface
|
---|
4 |
|
---|
5 | uses
|
---|
6 | SysUtils, Classes, Graphics, Controls, Forms, fgl, FileUtil,
|
---|
7 | StringTables, Directories
|
---|
8 | {$IFDEF WINDOWS}, MMSystem, Windows{$ENDIF}
|
---|
9 | {$IFDEF LINUX}, Process, AsyncProcess{$ENDIF};
|
---|
10 |
|
---|
11 | type
|
---|
12 | TPlayStyle = (psAsync, psSync);
|
---|
13 |
|
---|
14 | { TSoundPlayer }
|
---|
15 |
|
---|
16 | TSoundPlayer = class(TForm)
|
---|
17 | private
|
---|
18 | {$IFDEF WINDOWS}
|
---|
19 | PrevWndProc: WNDPROC;
|
---|
20 | procedure OnMCI(var m: TMessage); message MM_MCINOTIFY;
|
---|
21 | public
|
---|
22 | constructor Create(AOwner: TComponent); override;
|
---|
23 | {$ENDIF}
|
---|
24 | end;
|
---|
25 |
|
---|
26 | { TSound }
|
---|
27 |
|
---|
28 | TSound = class
|
---|
29 | private
|
---|
30 | {$IFDEF LINUX}
|
---|
31 | PlayCommand: string;
|
---|
32 | SoundPlayerAsyncProcess: TAsyncProcess;
|
---|
33 | SoundPlayerSyncProcess: TProcess;
|
---|
34 | {$ENDIF}
|
---|
35 | function GetNonWindowsPlayCommand: string;
|
---|
36 | public
|
---|
37 | FDeviceID: Word;
|
---|
38 | FFileName: string;
|
---|
39 | PlayStyle: TPlayStyle;
|
---|
40 | constructor Create(const FileName: string);
|
---|
41 | destructor Destroy; override;
|
---|
42 | procedure Play(HWND: DWORD);
|
---|
43 | procedure Stop;
|
---|
44 | procedure Reset;
|
---|
45 | end;
|
---|
46 |
|
---|
47 | function PrepareSound(FileName: string): Integer;
|
---|
48 | procedure PlaySound(FileName: string);
|
---|
49 | function Play(Item: string; Index: Integer = -1): Boolean;
|
---|
50 | procedure PreparePlay(Item: string; Index: Integer = -1);
|
---|
51 |
|
---|
52 | const
|
---|
53 | // sound modes
|
---|
54 | smOff = 0;
|
---|
55 | smOn = 1;
|
---|
56 | smOnAlt = 2;
|
---|
57 |
|
---|
58 | var
|
---|
59 | Sounds: TStringTable;
|
---|
60 | SoundMode: Integer;
|
---|
61 | SoundPlayer: TSoundPlayer;
|
---|
62 | SoundList: TFPGObjectList<TSound>;
|
---|
63 | PlayingSound: TSound;
|
---|
64 |
|
---|
65 |
|
---|
66 | implementation
|
---|
67 |
|
---|
68 | {$R *.lfm}
|
---|
69 |
|
---|
70 | resourcestring
|
---|
71 | SUnableToPlay = 'PlayStyle=%s: Unable to play %s Message:%s';
|
---|
72 | SPlayCommandNotWork = 'The play command %s does not work on your system';
|
---|
73 |
|
---|
74 | constructor TSound.Create(const FileName: string);
|
---|
75 | {$IFDEF WINDOWS}
|
---|
76 | var
|
---|
77 | OpenParm: TMCI_Open_Parms;
|
---|
78 | {$ENDIF}
|
---|
79 | begin
|
---|
80 | PlayStyle := psAsync;
|
---|
81 | FFileName := FileName;
|
---|
82 | {$IFDEF WINDOWS}
|
---|
83 | FDeviceID := 0;
|
---|
84 | if FileExists(FFileName) then begin
|
---|
85 | OpenParm.dwCallback := 0;
|
---|
86 | OpenParm.lpstrDeviceType := 'WaveAudio';
|
---|
87 | OpenParm.lpstrElementName := PChar(FFileName);
|
---|
88 | mciSendCommand(0, MCI_Open, MCI_WAIT or MCI_OPEN_ELEMENT or
|
---|
89 | MCI_OPEN_SHAREABLE, DWORD_PTR(@OpenParm));
|
---|
90 | FDeviceID := OpenParm.wDeviceID;
|
---|
91 | end
|
---|
92 | {$ENDIF}
|
---|
93 | {$IFDEF LINUX}
|
---|
94 | PlayCommand := GetNonWindowsPlayCommand;
|
---|
95 | FDeviceID := 1;
|
---|
96 | {$ENDIF}
|
---|
97 | end;
|
---|
98 |
|
---|
99 | destructor TSound.Destroy;
|
---|
100 | begin
|
---|
101 | {$IFDEF WINDOWS}
|
---|
102 | if FDeviceID <> 0 then
|
---|
103 | mciSendCommand(FDeviceID, MCI_CLOSE, MCI_WAIT, 0);
|
---|
104 | {$ENDIF}
|
---|
105 | {$IFDEF LINUX}
|
---|
106 | FreeAndNil(SoundPlayerSyncProcess);
|
---|
107 | FreeAndNil(SoundPlayerAsyncProcess);
|
---|
108 | {$ENDIF}
|
---|
109 | inherited;
|
---|
110 | end;
|
---|
111 |
|
---|
112 | function TSound.GetNonWindowsPlayCommand: string;
|
---|
113 | begin
|
---|
114 | Result := '';
|
---|
115 | // Try play
|
---|
116 | if (FindDefaultExecutablePath('play') <> '') then
|
---|
117 | Result := 'play';
|
---|
118 | // Try aplay
|
---|
119 | if (result = '') then
|
---|
120 | if (FindDefaultExecutablePath('aplay') <> '') then
|
---|
121 | Result := 'aplay -q';
|
---|
122 | // Try paplay
|
---|
123 | if (Result = '') then
|
---|
124 | if (FindDefaultExecutablePath('paplay') <> '') then
|
---|
125 | Result := 'paplay';
|
---|
126 | // Try mplayer
|
---|
127 | if (Result = '') then
|
---|
128 | if (FindDefaultExecutablePath('mplayer') <> '') then
|
---|
129 | Result := 'mplayer -really-quiet';
|
---|
130 | // Try CMus
|
---|
131 | if (Result = '') then
|
---|
132 | if (FindDefaultExecutablePath('CMus') <> '') then
|
---|
133 | Result := 'CMus';
|
---|
134 | // Try pacat
|
---|
135 | if (Result = '') then
|
---|
136 | if (FindDefaultExecutablePath('pacat') <> '') then
|
---|
137 | Result := 'pacat -p';
|
---|
138 | // Try ffplay
|
---|
139 | if (Result = '') then
|
---|
140 | if (FindDefaultExecutablePath('ffplay') <> '') then
|
---|
141 | result := 'ffplay -autoexit -nodisp';
|
---|
142 | // Try cvlc
|
---|
143 | if (Result = '') then
|
---|
144 | if (FindDefaultExecutablePath('cvlc') <> '') then
|
---|
145 | result := 'cvlc -q --play-and-exit';
|
---|
146 | // Try canberra-gtk-play
|
---|
147 | if (Result = '') then
|
---|
148 | if (FindDefaultExecutablePath('canberra-gtk-play') <> '') then
|
---|
149 | Result := 'canberra-gtk-play -c never -f';
|
---|
150 | // Try Macintosh command?
|
---|
151 | if (Result = '') then
|
---|
152 | if (FindDefaultExecutablePath('afplay') <> '') then
|
---|
153 | Result := 'afplay';
|
---|
154 | end;
|
---|
155 |
|
---|
156 |
|
---|
157 | procedure TSound.Play(HWND: DWORD);
|
---|
158 | {$IFDEF WINDOWS}
|
---|
159 | var
|
---|
160 | PlayParm: TMCI_Play_Parms;
|
---|
161 | {$ENDIF}
|
---|
162 | {$IFDEF LINUX}
|
---|
163 | var
|
---|
164 | L: TStringList;
|
---|
165 | I: Integer;
|
---|
166 | {$ENDIF}
|
---|
167 | begin
|
---|
168 | {$IFDEF WINDOWS}
|
---|
169 | if FDeviceID <> 0 then
|
---|
170 | begin
|
---|
171 | PlayParm.dwCallback := HWND;
|
---|
172 | mciSendCommand(FDeviceID, MCI_PLAY, MCI_NOTIFY, DWORD_PTR(@PlayParm));
|
---|
173 | end
|
---|
174 | {$ENDIF}
|
---|
175 | {$IFDEF LINUX}
|
---|
176 | // How to play in Linux? Use generic Linux commands
|
---|
177 | // Use asyncprocess to play sound as SND_ASYNC
|
---|
178 | // proceed if we managed to find a valid command
|
---|
179 | if PlayCommand <> '' then begin
|
---|
180 | L := TStringList.Create;
|
---|
181 | try
|
---|
182 | L.Delimiter := ' ';
|
---|
183 | L.DelimitedText := PlayCommand;
|
---|
184 | if PlayStyle = psASync then begin
|
---|
185 | if SoundPlayerAsyncProcess = nil then
|
---|
186 | SoundPlayerAsyncProcess := TAsyncProcess.Create(nil);
|
---|
187 | SoundPlayerAsyncProcess.CurrentDirectory := ExtractFileDir(FFilename);
|
---|
188 | SoundPlayerAsyncProcess.Executable := FindDefaultExecutablePath(L[0]);
|
---|
189 | SoundPlayerAsyncProcess.Parameters.Clear;
|
---|
190 | for I := 1 to L.Count - 1 do
|
---|
191 | SoundPlayerAsyncProcess.Parameters.Add(L[I]);
|
---|
192 | SoundPlayerAsyncProcess.Parameters.Add(FFilename);
|
---|
193 | try
|
---|
194 | SoundPlayerAsyncProcess.Execute;
|
---|
195 | except
|
---|
196 | On E: Exception do
|
---|
197 | E.CreateFmt(SUnableToPlay, ['paASync', FFilename, E.Message]);
|
---|
198 | end;
|
---|
199 | PlayingSound := nil;
|
---|
200 | end else begin
|
---|
201 | if SoundPlayerSyncProcess = nil then
|
---|
202 | SoundPlayerSyncProcess := TProcess.Create(nil);
|
---|
203 | SoundPlayerSyncProcess.CurrentDirectory := ExtractFileDir(FFilename);
|
---|
204 | SoundPlayerSyncProcess.Executable := FindDefaultExecutablePath(L[0]);
|
---|
205 | SoundPlayersyncProcess.Parameters.Clear;
|
---|
206 | for I := 1 to L.Count - 1 do
|
---|
207 | SoundPlayerSyncProcess.Parameters.Add(L[I]);
|
---|
208 | SoundPlayerSyncProcess.Parameters.Add(FFilename);
|
---|
209 | try
|
---|
210 | SoundPlayerSyncProcess.Execute;
|
---|
211 | SoundPlayersyncProcess.WaitOnExit;
|
---|
212 | except
|
---|
213 | On E: Exception do
|
---|
214 | E.CreateFmt(SUnableToPlay, ['paSync', FFilename, E.Message]);
|
---|
215 | end;
|
---|
216 | PlayingSound := nil;
|
---|
217 | end;
|
---|
218 | finally
|
---|
219 | L.Free;
|
---|
220 | end;
|
---|
221 | end
|
---|
222 | else
|
---|
223 | raise Exception.CreateFmt(SPlayCommandNotWork, [PlayCommand]);
|
---|
224 | {$ENDIF}
|
---|
225 | end;
|
---|
226 |
|
---|
227 | procedure TSound.Stop;
|
---|
228 | begin
|
---|
229 | {$IFDEF WINDOWS}
|
---|
230 | mciSendCommand(FDeviceID, MCI_STOP, 0, 0);
|
---|
231 | {$ENDIF}
|
---|
232 | {$IFDEF LINUX}
|
---|
233 | if SoundPlayerSyncProcess <> nil then SoundPlayerSyncProcess.Terminate(1);
|
---|
234 | if SoundPlayerAsyncProcess <> nil then SoundPlayerAsyncProcess.Terminate(1);
|
---|
235 | {$ENDIF}
|
---|
236 | end;
|
---|
237 |
|
---|
238 | procedure TSound.Reset;
|
---|
239 | begin
|
---|
240 | {$IFDEF WINDOWS}
|
---|
241 | mciSendCommand(FDeviceID, MCI_SEEK, MCI_SEEK_TO_START, 0);
|
---|
242 | {$ENDIF}
|
---|
243 | end;
|
---|
244 |
|
---|
245 | {$IFDEF WINDOWS}
|
---|
246 | function WndCallback(Ahwnd: HWND; uMsg: UINT; wParam: WParam; lParam: LParam):LRESULT; stdcall;
|
---|
247 | var
|
---|
248 | Message: TMessage;
|
---|
249 | begin
|
---|
250 | if (uMsg = MM_MCINOTIFY) then begin
|
---|
251 | Message.msg := uMsg;
|
---|
252 | Message.wParam := wParam;
|
---|
253 | Message.lParam := lParam;
|
---|
254 | SoundPlayer.OnMCI(Message);
|
---|
255 | end;
|
---|
256 | Result := CallWindowProc(SoundPlayer.PrevWndProc, Ahwnd, uMsg, WParam, LParam);
|
---|
257 | end;
|
---|
258 |
|
---|
259 | procedure TSoundPlayer.OnMCI(var m: TMessage);
|
---|
260 | begin
|
---|
261 | if (m.wParam = MCI_NOTIFY_SUCCESSFUL) and (PlayingSound <> nil) then
|
---|
262 | begin
|
---|
263 | PlayingSound.Reset;
|
---|
264 | PlayingSound := nil;
|
---|
265 | end;
|
---|
266 | end;
|
---|
267 |
|
---|
268 | constructor TSoundPlayer.Create(AOwner: TComponent);
|
---|
269 | begin
|
---|
270 | inherited;
|
---|
271 | // MM_MCINOTIFY is not handled by LCL, fallback to low lever handling
|
---|
272 | // https://wiki.lazarus.freepascal.org/Win32/64_Interface#Processing_non-user_messages_in_your_window
|
---|
273 | PrevWndProc := Windows.WNDPROC(SetWindowLongPtr(Self.Handle, GWL_WNDPROC, PtrInt(@WndCallback)));
|
---|
274 | end;
|
---|
275 | {$ENDIF}
|
---|
276 |
|
---|
277 | function PrepareSound(FileName: string): Integer;
|
---|
278 | begin
|
---|
279 | Result := 0;
|
---|
280 | while (Result < SoundList.Count) and (SoundList[result].FFileName <> FileName) do
|
---|
281 | Inc(Result);
|
---|
282 | if Result = SoundList.Count then begin
|
---|
283 | // First time this sound is played
|
---|
284 | SoundList.Add(TSound.Create(FileName));
|
---|
285 | Result := SoundList.Count - 1;
|
---|
286 | end;
|
---|
287 | end;
|
---|
288 |
|
---|
289 | procedure PlaySound(FileName: string);
|
---|
290 | begin
|
---|
291 | if PlayingSound <> nil then Exit;
|
---|
292 | if SoundPlayer = nil then
|
---|
293 | Application.CreateForm(TSoundPlayer, SoundPlayer);
|
---|
294 | PlayingSound := SoundList[PrepareSound(FileName)];
|
---|
295 | if PlayingSound.FDeviceID = 0 then
|
---|
296 | PlayingSound := nil
|
---|
297 | else
|
---|
298 | PlayingSound.Play(SoundPlayer.Handle);
|
---|
299 | end;
|
---|
300 |
|
---|
301 | function Play(Item: string; Index: Integer = -1): Boolean;
|
---|
302 | var
|
---|
303 | WavFileName: string;
|
---|
304 | begin
|
---|
305 | Result := False;
|
---|
306 | if (Sounds = nil) or (SoundMode = smOff) or (Item = '') then
|
---|
307 | begin
|
---|
308 | Result := True;
|
---|
309 | Exit;
|
---|
310 | end;
|
---|
311 | WavFileName := Sounds.Lookup(Item, Index);
|
---|
312 | Assert(WavFileName[1] <> '[');
|
---|
313 | Result := (WavFileName <> '') and (WavFileName[1] <> '[') and (WavFileName <> '*');
|
---|
314 | if Result then
|
---|
315 | // SndPlaySound(pchar(GetSoundsDir + DirectorySeparator + WavFileName + '.wav'), SND_ASYNC)
|
---|
316 | PlaySound(GetSoundsDir + DirectorySeparator + WavFileName);
|
---|
317 | end;
|
---|
318 |
|
---|
319 | procedure PreparePlay(Item: string; Index: Integer = -1);
|
---|
320 | var
|
---|
321 | WavFileName: string;
|
---|
322 | begin
|
---|
323 | if (Sounds = nil) or (SoundMode = smOff) or (Item = '') then
|
---|
324 | Exit;
|
---|
325 | WavFileName := Sounds.Lookup(Item, Index);
|
---|
326 | Assert(WavFileName[1] <> '[');
|
---|
327 | if (WavFileName <> '') and (WavFileName[1] <> '[') and (WavFileName <> '*') then
|
---|
328 | PrepareSound(GetSoundsDir + DirectorySeparator + WavFileName);
|
---|
329 | end;
|
---|
330 |
|
---|
331 | procedure UnitInit;
|
---|
332 | begin
|
---|
333 | SoundList := TFPGObjectList<TSound>.Create;
|
---|
334 | PlayingSound := nil;
|
---|
335 | SoundPlayer := nil;
|
---|
336 | end;
|
---|
337 |
|
---|
338 | procedure UnitDone;
|
---|
339 | begin
|
---|
340 | if PlayingSound <> nil then begin
|
---|
341 | PlayingSound.Stop;
|
---|
342 | Sleep(222);
|
---|
343 | end;
|
---|
344 | FreeAndNil(SoundList);
|
---|
345 | if Sounds <> nil then
|
---|
346 | FreeAndNil(Sounds);
|
---|
347 | end;
|
---|
348 |
|
---|
349 | initialization
|
---|
350 |
|
---|
351 | UnitInit;
|
---|
352 |
|
---|
353 | finalization
|
---|
354 |
|
---|
355 | UnitDone;
|
---|
356 |
|
---|
357 | end.
|
---|