0

I am somewhat new to programming. I am building a simple app using Kivy framework. The app has 15 buttons. When you press the button, it starts playing a melody. If any other button is playing at that moment, it should stop, i.e. multiple sounds (from different instances) should not play at the same time.

This is what I did — I created a class method that handles stopping and playing sounds. On press, the button instance passes sound object to this class method. However, on first press, since the sound object does not yet exist my app crashes. Hence the reason for try … except AttributeError part.

class Gumb(Button):
    soundfile = StringProperty(None)
    sound = ObjectProperty(None)
    now_playing_object = None

    def on_soundfile(self, instance, value):
        self.sound = SoundLoader.load(value)

    def on_press(self):
        if self.sound:
            self.__class__.play_sound(self.sound)

    @classmethod
    def play_sound(cls, new_sound_object):
        try:
            if cls.now_playing_object.state != 'stop':
                cls.now_playing_object.stop()
        except AttributeError:
            pass
        cls.now_playing_object = new_sound_object
        cls.now_playing_object.play()

This works, however I do not like it, especially thetry … except part. There has to be a better way to do this. Any ideas?

Thanks!

Update 1:

@toto_tico provided me with a couple of solutions to circumvent the try ... except part.

  1. Use if instead of exception:

    if cls.now_playing_object != None and cls.now_playing_object.state != 'stop':
        cls.now_playing_object.stop()
    

    Although, after reading about efficiency of python's ifs vs exceptions, I am not sure that I should be trying to eliminate try ... except at all.

  2. Initialize the now_playing_object on the very beginning. To do this I used:

    now_playing_object = SoundLoader.load(‘non_existant_file.wav’)
    

    This works, but it feels a bit unreliable (it is strange that Kivy complains if the filename misses extension, but not if there is no file at all). Nevertheless, I think I'll use this one.

Update 2:

It turns out that the solution was right in front of me. I just couldn't see it. There was really no need for @classmethod:

class Gumb(Button):
...
    def play_sound(self):
        if self.__class__.now_playing_object.state is not 'stop':
            self.__class__.now_playing_object.stop()
        self.__class__.now_playing_object = self.sound
        self.__class__.now_playing_object.play()

I've accepted the answer, but the real answer is in the comments. Thanks @toto_tico.

blablatros
  • 733
  • 1
  • 7
  • 21

2 Answers2

2

You can just check if now_playing_object is None.

Instead of:

try:
    if cls.now_playing_object.state != 'stop':
       cls.now_playing_object.stop()
except AttributeError:
    pass
...

You should be able to do this:

if cls.now_playing_object != None and cls.now_playing_object.state != 'stop':
    cls.now_playing_object.stop()
...

Hope it helps.

toto_tico
  • 17,977
  • 9
  • 97
  • 116
  • 1
    Thanks, I learned something new today — it is safe to rely on the order of conditionals. What about [this](http://stackoverflow.com/a/752396/1079495)? – blablatros Jul 24 '13 at 20:53
  • 1
    More than just the order, it is efficiency. The second condition never executes because the first being `False` is enough to decide that the condition is going to be False, no matter the value of the next statements. This could lead to hard-to-catch bugs when you expect that all the instructions to be executed. For example, `if foo or and_important_method():`. In this case it is an or, so if foo is `True`, then `an_important_method()` is never called and you were assuming that. – toto_tico Jul 25 '13 at 01:08
  • Speaking of efficiency — given that `now_playing_object` is `None` only the first time that the `play_sound` method is called — wouldn't it be more efficient to use `try … except` instead of evaluating whether the `cls.now_playing_object is not None` each time (since it is first condition)? – blablatros Jul 25 '13 at 12:39
  • 1
    The `if` statement is more efficient. Try/catch requires some global management and are meant to use when the `if` is not enough. The most common example is when you open a file. You can use an `if` to verify it exist but between the `if` and the `open` things could happen in the machine. Another process (in another core or just because a change of context in the cpu - multitasking) could delete the file after your `if` and it is not there any more. In summary, `ifs` are cheaper but sometimes in live the best we can do is just `try`. In any case, the difference is meaningless. – toto_tico Jul 25 '13 at 13:33
  • 1
    Not that it matters in this case – as the difference is, as you say, meaningless – but it seems that Python’s exceptions are actually cheaper than ifs. Take a look [here](http://stackoverflow.com/questions/1835756/using-try-vs-if-in-python) and [here](http://stackoverflow.com/questions/3086806/python-exceptions-eafp-and-what-is-really-exceptional). However, if does look more readable to me. – blablatros Jul 26 '13 at 14:54
  • 1
    Thanks, I learned something new today. Python is like some religion out there - '...easier to ask for forgiveness than permission'. – toto_tico Jul 27 '13 at 01:26
1

To take out the if condition:

If you don't want to check, then you should the initialize the variable since the very beginning. I don't know the class that you are using for now_playing_object but lets call it PlayingObject.

Instead of:

now_playing_object = None

You should initialize it with :

#maybe needs parameters or simple use now_playing_object = ObjectProperty(None)
now_playing_object = Sound() 

And then you can take out the condition of my first answer (cls.now_playing_object != None) because the object is always initialize.

To take out the @classmethod:

I found interesting that you were using @classmethod. I have been couple of years in Python and honestly it was completely new for me. There are several answers depending what you are trying to do. I will assume that you are clear with the difference between class and instance in Oriented-Object Programming.

You current code works if (1) you just have an instance of the Gumb class or (2) you have several instances of the Gumb class but all of them controls the same soundfile, sound and now_playing_object.

Now, if you want your 15 Gumb buttons controlling different soundfile, sound and now_playing_object, then you shouldn't be using the @classmethod, neither the way you are declaring your attributes (because you are declaring them as class attributes and you need them to be part of the instance). The code will look more like this:

class Gumb(Button):
    # Constructor. 
    def __init__(self,  **kwargs):
        super(Gump, self).__init__(**kwargs)
        #This became attributes of the 
        self.soundfile = StringProperty(None)\
        self.sound = ObjectProperty(None)
        self.now_playing_object = ObjectProperty(None) 

    def on_soundfile(self, instance, value):
        self.sound = SoundLoader.load(value)

    def on_press(self):
        if self.sound:
            self.play_sound()

    def play_sound(self):
        if self.now_playing_object.state != 'stop':
            self.now_playing_object.stop()

        # self.sound was your new_playing_object, since it is part of the instance
        # you don't need to send it as parameter
        self.now_playing_object = self.sound
        self.now_playing_object.play()

Is that more similar to what you want?

toto_tico
  • 17,977
  • 9
  • 97
  • 116
  • *Regarding the* `if`: `now_playing_object` and `new_sound_object` belong to the same class as `self.sound`. I could initialize `now_playing_object` with `now_playing_object = SoundLoader.load(‘non_existant_file.wav’)`. This works, but it feels a bit unreliable (it is strange that Kivy complains if the filename misses extension, but not if there is no file at all). I guess there is no perfect solution :) – blablatros Jul 27 '13 at 13:22
  • 1
    *Regarding* `@classmethod`: My current solution actually works, and `Gumb` instances **do** control different `soundfile` and `sound`. I was under the impression that Kivy properties (e.g. `StringProperty` and `ObjectProperty`) are **not** class attributes -- [look here](http://kivy.org/docs/api-kivy.properties.html#comparison-python-kivy). – blablatros Jul 27 '13 at 15:03
  • 1
    That is very interesting, sorry to mislead you. Your `now_playing_object` is still a class attribute. All the instances share access to the current playing. I thought that each button should be in charge (start and stop) one track. The `@classmethod` is still not necessary (but still correctly used) because you can always access class attributes with `self.__class__.now_playing_sound` and then have a 'normal' method. I think you already got your perfect solution since the beginning. – toto_tico Jul 28 '13 at 06:21
  • The solution you propose will stop the sound that that particular Gumb instance is playing, but not sounds from other instances, i.e. multiple sounds (each from different instance) can play at the same time. – blablatros Jul 28 '13 at 20:16
  • `self.__class__.now_playing_object` => This is exactly what I needed. Thanks! – blablatros Jul 28 '13 at 20:17