0

I am writing a music database manager using python and tkinter. It is nearly all complete but I am stuck on playing songs from a list. The native macOS player NSSound has a built-in callback to a delegate which signals the end of the track so I would like to use that to trigger a virtual event back to the main app to send the next song to play. But I cannot figure out how make the callback work and would appreciate some help (this being just a hobby project which has got somewhat out of hand).

This skeleton code shows the relevant structure of the app and plays the selected file as expected, but none of my numerous attempts at formulating the delegate have worked. As I understand it the player appoints the Delegate class as its delegate, and then this class should have a callback method 'sound' to receive a message when the song ends. I have tried to figure it out from the Apple Developer protocol, and also searched extensively and found only one example.

How do I do get to the 'Success!' message please?

from AppKit import NSSound
import tkinter as tk
from tkinter import filedialog
       
class Delegate (NSSound):
    def init(self):
        self = super(Delegate, self).init()
        return self        
    #def sound (...) ???   #this should fire when the song finishes?
    #   print('Success!')
    
class App(tk.Tk):  #representing the rest of the music database app
    def __init__(self):
        super().__init__()         
        file = filedialog.askopenfilename()          
        song = NSSound.alloc()
        song.initWithContentsOfFile_byReference_(file, True) 
        delegate = Delegate.alloc().init()
        song.setDelegate_(delegate)      
        song.play()
           
if __name__ == "__main__":
    app = App()
    app.mainloop()
  
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • are you sure you should use `init()` instead of `__init__()` ? – furas Nov 12 '21 at 23:43
  • You're right for a Python object, but if Delegate is to be an Objective C object (which I am not sure about) then it seems the two-step method I have shown is needed. I tried making a Python object but that does not work either. – user10850186 Nov 14 '21 at 01:51

2 Answers2

0

The NSSound instance will call a method with this (swift) signature on its delegate:

sound(_:didFinishPlaying:)

Thus I think your Python function should have the same signature: it’s name should be sound and it should accept two parameters, the first one without a label (_) of type NSSound, the second one of type Bool with a label didFinishPlaying.

Your sound function in your delegate doesn’t appear to have such signature, hence most probably it’s not called.

That is cause such method on the delegate is optional, meaning if an instance conforming to NSSoundDelegate doesn’t implement such method, it won’t trap when attempted to be used by a delegating NSSound instance.

valeCocoa
  • 344
  • 1
  • 8
  • thanks for your suggestion but if I put this ` def sound (_ : NSSound, didFinishPlaying : bool) :` then I get this message: `objc.BadPrototypeError: Objective-C expects 1 arguments, Python argument has 2 arguments for – user10850186 Nov 14 '21 at 01:54
  • How about `def sound(s, *didFinishPlaying):`? Does it work? – valeCocoa Nov 14 '21 at 03:11
  • That gives a quite mystifying message: ` ..Objective-C expects 1 arguments, Python argument has 1 arguments for – user10850186 Nov 14 '21 at 03:58
  • Ok maybe I got it: your python class should inherit from `NSObject` and conform to `NSSoundDelegate`. How you do that with PyObjC I frankly have no idea (perhaps you should import `Cocoa` and name the class as in `Class MyDelegate(Cocoa.NSObject)` but still I don't know how to also mark it as conforming to `NSSoundDelegate` protocol). The naming convention for the python method should be as in `sound_didFinishPlaying(self, sound, didFinish):` – valeCocoa Nov 14 '21 at 18:16
  • I found that the protocol goes like this `NSSoundDelegate = objc.protocolNamed('NSSoundDelegate')`, followed by `class Delegate (NSSound , protocols = [NSSoundDelegate]): `. Good suggestion on the method name and it just needs a trailing underscore to avoid the same errors - ` sound_didFinishPlaying_` . But still no success message! – user10850186 Nov 15 '21 at 04:31
  • Does the method get called with that signature? Otherwise you're still having the issue that it does not have the right signature still. I'm sorry I couldn't be more helpful but I'm not so fond of Python nor of PyObjC. – valeCocoa Nov 15 '21 at 12:47
0

Thanks for your help @valeCocoa, your perserverance helped a lot. This works:

from AppKit import NSSound
import tkinter as tk
from tkinter import filedialog
import objc

class Song (NSSound) : 
    def initWithFile_(self, file): 
        self = objc.super(Song, self).initWithContentsOfFile_byReference_(file, True) 
        if self is None: return None
        self.setDelegate_(self)
        return self    
    def sound_didFinishPlaying_(self, _, didFinish):
        print('Success')

class App(tk.Tk):  #representing the rest of the music database app
    def __init__(self):
        super().__init__()         
        file = filedialog.askopenfilename()          
        song = Song.alloc().initWithFile_(file)
        song.play()

if __name__ == "__main__":
    app = App()
    app.mainloop()