unit uAlarmSound; { Suoni di allarme delle spie. Usa MCI (mciSendString), che su Windows suona MP3 e WAV senza librerie esterne e senza mettere un componente sul form. La riproduzione non blocca: "play" torna subito e il suono continua per conto suo, quindi una plancia che suona resta comandabile. Ogni file suona su un proprio alias, aperto la prima volta e tenuto aperto: riaprirlo a ogni allarme costerebbe qualche decimo di secondo, e un cicalino deve partire nell'istante in cui la spia si accende. Due allarmi diversi si sovrappongono; lo stesso allarme che si ripete riparte da capo. } interface uses Winapi.Windows, Winapi.MMSystem, System.SysUtils, System.Classes, System.Generics.Collections; type TAlarmPlayer = class private /// Percorso del file -> alias MCI aperto per quel file. FOpen: TDictionary; FNextAlias: Integer; FLastError: string; function Command(const ACommand: string): Boolean; /// Comando MCI che torna una risposta ('status ... mode' e simili). function Query(const ACommand: string): string; function AliasFor(const AFileName: string): string; public constructor Create; destructor Destroy; override; /// Suona il file, ripartendo da capo se sta gia' suonando. False se non /// si e' potuto suonare; il motivo sta in LastError. /// Con ALoop il suono riparte da solo quando finisce, finche' non lo si /// ferma: un allarme deve continuare a chiamare, non suonare una volta. function Play(const AFileName: string; ALoop: Boolean = False): Boolean; /// Vero se quel file sta suonando in questo momento. Serve a far /// ricominciare l'allarme sui dispositivi che non sanno ripetere da soli. function IsPlaying(const AFileName: string): Boolean; /// Ferma un suono solo, lasciando suonare gli altri: serve quando una /// spia si spegne mentre altri allarmi sono ancora attivi. procedure Stop(const AFileName: string); /// Zittisce tutto: si usa lasciando la plancia o chiudendo. procedure StopAll; property LastError: string read FLastError; end; implementation const /// Oltre questo numero di suoni diversi aperti, il piu' vecchio si chiude: /// una plancia con decine di allarmi non deve tenere aperti decine di /// dispositivi MCI. MAX_OPEN = 8; { TAlarmPlayer } constructor TAlarmPlayer.Create; begin inherited Create; FOpen := TDictionary.Create; end; destructor TAlarmPlayer.Destroy; begin StopAll; FOpen.Free; inherited; end; function TAlarmPlayer.Command(const ACommand: string): Boolean; var Err: MCIERROR; Buf: array[0..255] of Char; begin Err := mciSendString(PChar(ACommand), nil, 0, 0); Result := Err = 0; if Result then Exit; if mciGetErrorString(Err, Buf, Length(Buf)) then FLastError := Buf else FLastError := Format('errore MCI %d', [Err]); end; function TAlarmPlayer.AliasFor(const AFileName: string): string; var Key: string; Old: TPair; begin Key := LowerCase(AFileName); if FOpen.TryGetValue(Key, Result) then Exit; if FOpen.Count >= MAX_OPEN then for Old in FOpen do begin Command('close ' + Old.Value); FOpen.Remove(Old.Key); Break; end; Inc(FNextAlias); Result := Format('plancia_all%d', [FNextAlias]); // Il tipo va detto: senza, MCI sceglie dall'estensione e sui .mp3 di certe // installazioni sbaglia dispositivo. if not Command(Format('open "%s" type mpegvideo alias %s', [AFileName, Result])) then if not Command(Format('open "%s" alias %s', [AFileName, Result])) then Exit(''); FOpen.Add(Key, Result); end; function TAlarmPlayer.Query(const ACommand: string): string; var Buf: array[0..127] of Char; begin if mciSendString(PChar(ACommand), Buf, Length(Buf), 0) = 0 then Result := Buf else Result := ''; end; function TAlarmPlayer.IsPlaying(const AFileName: string): Boolean; var Alias: string; begin Result := FOpen.TryGetValue(LowerCase(AFileName), Alias) and SameText(Query('status ' + Alias + ' mode'), 'playing'); end; function TAlarmPlayer.Play(const AFileName: string; ALoop: Boolean): Boolean; var Alias: string; begin FLastError := ''; if AFileName = '' then Exit(False); if not FileExists(AFileName) then begin FLastError := 'file non trovato'; Exit(False); end; Alias := AliasFor(AFileName); if Alias = '' then Exit(False); // "from 0" fa ripartire da capo un allarme che sta gia' suonando: meglio // risentirlo dall'inizio che non sentire niente. Con "repeat" il // dispositivo ripete da solo; non tutti lo accettano, e a quelli si rimedia // facendolo ripartire da fuori quando risulta fermo (vedi IsPlaying). Result := False; if ALoop then Result := Command(Format('play %s from 0 repeat', [Alias])); if not Result then Result := Command(Format('play %s from 0', [Alias])); if not Result then begin // Il dispositivo puo' essere morto (cuffie staccate): si riapre da capo. Command('close ' + Alias); FOpen.Remove(LowerCase(AFileName)); Alias := AliasFor(AFileName); if Alias <> '' then Result := Command(Format('play %s from 0', [Alias])); end; end; procedure TAlarmPlayer.Stop(const AFileName: string); var Alias: string; begin // Il dispositivo resta aperto: se l'allarme torna, riparte senza il ritardo // dell'apertura. if FOpen.TryGetValue(LowerCase(AFileName), Alias) then Command('stop ' + Alias); end; procedure TAlarmPlayer.StopAll; var Alias: string; begin for Alias in FOpen.Values do begin Command('stop ' + Alias); Command('close ' + Alias); end; FOpen.Clear; end; end.