Delphi MediaPlayer notification on song stopped - delphi

I need to embed a trivial MP3 player in a Delphi7 application.
I will simply scan a directory and play all the files in a random order.
I found two possible solutions: one using the Delphi MediaPlayer, and one using the PlaySound Windows API.
None are working.
The problem seems to be in the missing "stop" notification.
Using PlaySound like this:
playsound(pchar(mp3[r].name), 0, SND_ASYNC or SND_FILENAME);
I could not find a way to (politely) ask Windows to inform me when a song stopped playing.
Using the Delphi MediaPlayer, internet is full of suggestions copy/pasted one from the other, like here:
http://www.swissdelphicenter.ch/en/showcode.php?id=689
http://delphi.cjcsoft.net/viewthread.php?tid=44448
procedure TForm1.FormCreate(Sender: TObject);
begin
MediaPlayer1.Notify := True;
MediaPlayer1.OnNotify := NotifyProc;
end;
procedure TForm1.NotifyProc(Sender: TObject);
begin
with Sender as TMediaPlayer do
begin
case Mode of
mpStopped: {do something here};
end;
//must set to true to enable next-time notification
Notify := True;
end;
end;
{
NOTE that the Notify property resets back to False when a
notify event is triggered, so inorder for you to recieve
further notify events, you have to set it back to True as in the code.
for the MODES available, see the helpfile for MediaPlayer.Mode;
}
My problem is that I do get a NotifyValue == nvSuccessfull when a song is over but ALSO when I start a song, so I cannot rely on it.
Furthermore, I never, ever receive a change in state of the "mode" property, that should become mpStopped according to all the examples I found.
There is a similar question here
How can I repeat a song?
but it does not work because, as said, I receive the nvSuccessfull twice, without a way to distinguish between the start and the stop.
Last but not least, this app should work from XP to Win10, that's why I am developing with Delphi7 on WinXP.
Thank you and sorry for the length of this post, but I really tried many solutions before asking for help.

To detect when to load a new file for playing, you can use the OnNotify event and the EndPos and Position properties of the TMediaPlayer (hereafter called MP)
First setup the MP and select a TimeFormat, for example
MediaPlayer1.Wait := False;
MediaPlayer1.Notify := True;
MediaPlayer1.TimeFormat := tfFrames;
MediaPlayer1.OnNotify := NotifyProc;
When you load a file for playing, set the EndPos property
MediaPlayer1.FileName := OpenDialog1.Files[NextMedia];
MediaPlayer1.Open;
MediaPlayer1.EndPos := MediaPlayer1.Length;
MediaPlayer1.Play;
And the OnNotify() procedure
procedure TForm1.NotifyProc(Sender: TObject);
var
mp: TMediaPlayer;
begin
mp:= Sender as TMediaPlayer;
if not (mp.NotifyValue = TMPNotifyValues.nvSuccessful) then Exit;
if mp.Position >= mp.EndPos then
begin
// Select next file to play
NextMedia := (NextMedia + 1) mod OpenDialog1.Files.Count;
mp.FileName := OpenDialog1.Files[NextMedia];
mp.Open;
mp.EndPos := mp.Length;
mp.Position := 0;
mp.Play;
// Set Notify, important
mp.Notify := True;
end;
end;
Finally a comment to your attempt to use the MP.Mode = mpStopped mode to change to a new song. The mode is changed when the buttons are operated, iow mpStopped when the user presses the Stop button. Changing the song and starting to play it would likely not be what the user expected.

Related

Delphi, TEdit text as action trigger

I am using a TEdit to allow the user to enter a number, e.g. 10.
I convert the TEdit.Text to an integer and a calculation procedure is called.
In this calc procedure, a check was built-in to make sure no numbers below 10 are processed.
Currently I use the OnChange event. Suppose the user wants to change '10' into e.g.'50'. But as soon as the '10' is deleted or the '5' (to be followed by the 0) is typed, I trigger my warning that the minimum number is 10. I.e. the program won't wait until I have fully typed the 5 and 0.
I tried OnEnter, OnClick, OnExit, but I seem not to overcome this problem. The only solution is to add a separate button that will trigger the calculation with the new number. It works, but can I do without the button?
Use a timer for a delayed check, e.g.:
procedure TForm1.Edit1Change(Sender: TObject);
begin
// reset the timer
Timer1.Enabled := false;
Timer1.Enabled := true;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled := false;
// do your check here
end;
Setting the timer to 500 ms should be fine for most users.
And as David suggested in the comments to the question: Do not show an error dialog, use something less intrusive instead, e.g. an error message in a label near the edit or change the background color. And also: Do not prevent the focus to be moved away from that control and do not play a sound, that's also very annoying.
For our in house software we set the background of a control to yellow if there is an error and display the error message of the first such error in the status bar and also as a hint of the control. If you do that, you probably don't even need to have the delay.
Thanks, for your help. I tried the timer option, but could not get that to work. I now have this code, which works (almost - see below), but requires the used to always type a CR:
procedure Calculate;
begin
// this is my calculation procedure
ShowMessage('calculation running correctly');
end;
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
N : integer;
begin
if Key = #13 then
begin
N := StrtoInt(Edit1.Text);
if N<10 then ShowMessage('<10!') else
if N>100 then ShowMessage('>100!') else Calculate;
end;
end;
I used the ShowMessage() here only to see if the sample code worked. In the real program I have left that out, as you all suggested.
I also included the 'turn yellow on wrong entry' (thanks David). The only issue is that if the user runs this I get a beep from my computer. I can't see what went wrong. But what is causing the beep?

How do I prevent the user from clicking a button too often?

In a button-click event, I am sending a command to a server. I don't want the server to be flooded with requests, so I want to prevent the user from clicking the button too often.
I've thought about counting how many times the button is clicked, like this:
button.tag := button.tag + 1;
if button.Tag = 5 then
exit;
But this will not help because I have to set the tag to 0 each time I need to detect the fast clicks, and this will also not help because it will detect the tag number every 5 clicks, whereas I just need to know if the button was clicked repeatedly.
How do I detect fast button clicks without using the button tag?
If you want to restrict how often to send commands from your client to the server, disable the button a short while after each click. This will give the user feedback that something is processing. I'm assuming there is no way the server can give feedback when it is ready.
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := false;
Timer1.Interval := 500; // Pick a suitable interval
Timer1.Enabled := true;
SendCommand(); // Make call to server
end;
procedure TForm1.Timer1Timer(Sender: TObject);
// Timer1 is disabled at startup
begin
Button1.Enabled := true;
Timer1.Enabled := false;
end;
Note: if you are trying to do some load balancing on the server, there must be a way for the clients to know how to restrict the calls. But this is all a backwards way of doing it, the server should be capable of handling this without any restrictions on the clients.
You've correctly recognized that merely counting the clicks won't be sufficient. You need to account for the time, too. So use a timer.
When the button is clicked, disable the button and enable a timer. When the timer fires, re-enable the button.
procedure TExampleForm.SendButtonClick(Sender: TObject);
begin
Assert(SendButton = Sender);
SendButton.Enabled := False;
SendButtonTimer.Enabled := True;
SendCommandToServer(...);
end;
procedure TExampleForm.SendButtonTimerTimer(Sender: TObject);
begin
Assert(SendButtonTimer = Sender);
SendButtonTimer.Enabled := False;
SendButton.Enabled := True;
end;
Set the timer's Interval property to the number of milliseconds that you want the button to remain disabled.
There's no need to count the clicks in this scenario because the implicit limit is one click per timer interval.
You could just put some kind of timestamp into the Tag, and check whether enough time has elapsed.
Better yet, derive your own class, (TDontPressMeTooFastButton ;-), and you can do whatever you want, ie.:
track both how much time elapsed since last click and how many "fast clicks" the user attempted
you could make the button contain the timer component, not leaking this implementation detail into the form
Just about anything, in a much cleaner way than using the .Tag or global/form variables ...

Delphi XE3 WM_HOTKEY How to tell when HotKey is Released ?

I am trying to program a "cough button" in a communications program that does not always have focus. I have the code working to mute and unmute the mic (MMDevApi) and it works perfect. I set up a global hot key and this works perfect to set the mute. Now the problem. How do I tell when the Hotkey is released ? I tried a timer as shown in the code but it has an odd behavior. I Hold down my hotkey and the mic mutes immediately then after the interval of the timer it unmutes for a what appears to be half the interval of the timer and then it mutes and stays muted. Without the timer it mutes and stays muted perfectly. I really don't want ( or think it would be any good ) to have to hit a second key to unmute the mic.
//here is my register hot key code !
CoughKeyWnd := AllocateHwnd(CoughKeyWndProc);
CoughKey := GlobalAddAtom('CoughKey');
if CoughKey <> 0 then
RegisterHotKey(CoughKeyWnd, CoughKey, MOD_CONTROL, VK_OEM_3);
//the procedure
procedure TForm1.CoughKeyWndProc(var Message: TMessage);
begin
if Message.Msg = WM_HOTKEY then
begin // to prevent recalling mute
if CoughOn = FALSE then
begin
CoughOn := True;
CoughOff.SetMute(1,#GUID_NULL);
end;
Timer1.Enabled := FALSE;
Timer1.Enabled := True;
end
else
begin
Message.Result := DefWindowProc(CoughKeyWnd, Message.Msg, Message.WParam, Message.LParam);
end;
//and finally the ontimer !
procedure TForm1.JvTimer1Timer(Sender: TObject);
begin
CoughOff.SetMute(0,#GUID_NULL);
Timer1.Enabled := False;
CoughOn := False;
end;
You'll see that kind of behavior if your timer expires before the second WM_HOTKEY is retrieved but does not expire retrieving consecutive messages. The time frame between the first and second message is greater than the time frame between consecutive messages. This is because keyboard delay is greater (~250ms typical) than keyboard repeat interval.
To make your approach work, increase your timer interval, something like twice the keyboard delay for instance. You can use SystemParametersInfo to get an approximation for keyboard delay. Or employ a minimum time frame for the microphone to stay in muted state, and only after then start watching for repeated hotkey messages to re-enable your timer. Still, this method will be somewhat unreliable, hotkey messages might be delayed for whatever reason. Better use GetKeyState in your timer handler to test if the keys are still down.
You can install a keyboard hook or register for raw input when the hotkey is hit if you don't want to use the timer.

how to pause while some process going in loopss(delphi)

how can i pause and resume in application while working with loops
i can put some sleep(xxx) at begininsg of my loop to pause ,but i want pause when ever i want and resume when i ever i need
any ideas ?
thanks in advance
ok here is alittle more explanation
for i:=0 to 100 do
begin
if button1.clicked then pause //stop the operation and wait for resume button
if button2.clicked then resume //resume the operations
end;
edit2 :
ok guys i will tell an example well take any checker for suppose proxy checker ,i have a bunch of proxies loaded in my tlistviwe i am checking them all ,by using lop for i:=0 to listview.items.count do ......
i want to pause my checking operation when ever i want and resume when ever i need
am i clear or still i have to explain some ? :S
regards
You need a boolean flag that will indicate whether it's safe to continue looping. If something happens that makes it so you need to pause, it should set the variable to false. This means that, unless you're working with multiple threads, whatever sets this flag will have to be checked inside the loop somewhere. Then at the top (or the bottom) of your loop, check to see if this variable is true and pause otherwise.
Here's the basic idea, in the context of your explanation:
procedure TForm1.DoLoop;
begin
FCanContinue := true;
for i:=0 to 100 do
begin
//do whatever
Application.ProcessMessages; //allow the UI to respond to button clicks
if not FCanContinue then Pause;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FCanContinue := false;
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
FCanContinue := true;
Resume;
end;
This is a very simplistic implementation. What you really should do if you have a long-running task that you need to be able to pause and resume on command is put this in a separate thread and take a look at the TEvent class in SyncObjs.
OK, I don't understand what you are trying to do, but here is some pseudo code:
for i:=0 to 100 do
begin
if button1.clicked then
begin
while not button2.clicked do
sleep(50);
end;
end;
Check if a key is pressed?

Delphi Pascal Problem when WMDeviceChange function calls other functions/procedures

SOLVED
I am using delphi 2009. My program listens for usb drives being connected and remove. Ive used a very similar code in 10 apps over the past year. It has always worked perfectly. When i migrated i had to give up using thddinfo to get the drive model. This has been replaced by using WMI. The WMI query requires the physical disk number and i happen to already have a function in the app for doing just that.
As i test I put this in a button and ran it and it successfully determines the psp is physical drive 4 and returns the model (all checked in the debugger and in another example using show message):
function IsPSP(Drive: String):Boolean;
var
Model: String;
DriveNum: Byte;
begin
Result := False;
Delete(Drive, 2, MaxInt);
DriveNum := GetPhysicalDiskNumber(Drive[1]);
Model := (MagWmiGetDiskModel(DriveNum));
if Pos('PSP',Model) > 0 then Result := True;
end;
procedure TfrmMain.Button1Click(Sender: TObject);
var DriveNum: Byte;
begin
IsPSP('I');
end;
It works perfectly that is until i allow the WMDeviceChange that ive been using for a year to call up the getphysicaldisknumber and the wmi query statement. Ive tried them by themselves theyre both a problem. GetPhysicalDiskNumber freezes real bad when its doing a CloseHandle on the logical disk but does return the number eventually. The WMI query fails with no error just returns '' debugger points into the wbemscripting_tlb where the connection just never happened. Keep in mind the only thing thats changed in a year is what im calling to get the model i was using an api call and now im using something else.
Below is the rest of the code involved at this time sans the ispsp that is displayed above:
procedure TfrmMain.WMDeviceChange(var Msg: TMessage);
var Drive: String;
begin
case Msg.wParam of
DBT_DeviceArrival: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceInsert(Drive);
end;
DBT_DeviceRemoveComplete: if PDevBroadcastHdr(Msg.lParam)^.dbcd_devicetype = DBT_DevTyp_Volume then
begin
Drive := GetDrive(PDevBroadcastVolume(Msg.lParam)) + '\';
OnDeviceRemove(Drive);
end;
end;
end;
Procedure TfrmMain.OnDeviceInsert(Drive: String);
var PreviousIndex: Integer;
begin
if (getdrivetype(Pchar(Drive))=DRIVE_REMOVABLE) then
begin
PreviousIndex := cbxDriveList.Items.IndexOf(cbxDriveList.Text);
cbxDriveList.Items.Append(Drive);
if PreviousIndex = -1 then //If there was no drive to begin with then set index to 0
begin
PreviousIndex := 0;
cbxDriveList.ItemIndex := 0;
end;
if isPSP(Drive) then
begin
if MessageDlg('A PSP was detect # ' + Drive + #10#13 + 'Would you like to select this drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end
else if MessageDlg('USB Drive ' + Drive + ' Detected' + #10#13 + 'Is this your target drive?',mtWarning,[mbYes,mbNo], 0) = mrYes then
cbxDriveList.ItemIndex := cbxDriveList.Items.IndexOf(Drive)
else cbxDriveList.ItemIndex := PreviousIndex;
end;
end;
Procedure TfrmMain.OnDeviceRemove(Drive: String);
begin
if not (getdrivetype(Pchar(Drive)) = DRIVE_CDROM) then
begin
if cbxDriveList.Text = (Drive) then ShowMessage('The selected drive (' + Drive + ') has been removed');
cbxDriveList.Items.Delete(cbxDriveList.Items.IndexOf(Drive));
if cbxDriveList.Text = '' then cbxDriveList.ItemIndex := 0;
if Drive = PSPDrive then //Check Detect PSP and remove reference if its been removed
begin
PSPDrive := '';
end;
end;
end;
Rob has said something below about im not calling the inherited message handler, ive read the document i see a couple of things i can return... but im not really sure i understand but i will look into it. Im not a very good pascal programmer but ive been learning alot. The transition to 2009 has had some rough patches as well.
The USB drive detection and all that works perfectly. If i remove the two things from is psp the user is greeted right away with wis this your whatever and adds I:\ to the list. Its just the two new things that have changed in the app that fail when called by wmdevicechange and as said before they work on their own.
EDIT - SOLVED
Alright well im using a timer as suggested and the problem seems to be solved. One note is that when called by the timer very shortly after the wmdevicechange getting the physical disk number still seems to be slow. I attribute this to the device still being attached to the system.
On that note im using a P2 450 on the regular. I hooked the PSP and app to a 1.8Ghz Dual Core Laptop and the program detected the psp and notified the user very fast. So the app wont freeze unless there on a very very slow computer and on this slow onw its only for a matter of seconds and doesnt affect the operation of the program though isnt very cool. But i feel that all modern computers will run the detection fast especially because they can attach the device alot faster.
It's possible that the information you're querying becomes available only after the WMDeviceChange message handler runs. If the very same code works when called from a button, try this:
Refactor your WMDeviceChange handler code into one or more separate methods.
In the WMDeviceChange handler, activate a precreated timer and have it fire one second later, or something like that.
Call the former WMDeviceChange handler code from the timer handler code.
You haven't indicated what "statement 1" is in your code.
I have a few comments about parts of the code, which may or may not be related to the problem you're having.
First, you assign a value to DriveNum in IsPSP, but you don't use it. The compiler should have issued a hint about that; don't ignore hints and warnings. You also pass the magic number 4 into MagWmiGetDiskModel; was that supposed to be DriveNum instead?
You aren't calling the inherited message handler, and you aren't returning a result in your message handler. The documentation tells what values you're supposed to return. To return a value from a Delphi message handler, assign a value to the Msg.Result field. For the cases that your message handler doesn't handle, make sure you call inherited so that the next handler up the chain can take care of them. If there is no next handler, then Delphi will call DefWindowProc to get the operating system's default behavior.
The change you've illustrated is called refactoring, and it will do nothing to affect how your code runs. It makes the code easier to read, though, so please keep the second version. As for finding the problem, my best advice is to use the debugger to step through the code to identify the point where things stat to go wrong and the parts that run slower than you'd like. You can also try removing portions of the code to confirm that the other parts work correctly in isolation.

Resources