bbcmicrobit / micropython

Port of MicroPython for the BBC micro:bit
https://microbit-micropython.readthedocs.io
Other
602 stars 284 forks source link

Add a music module #31

Closed ntoll closed 8 years ago

ntoll commented 8 years ago

It should be possible to make sweet music using MicroPython on the micro:bit.

To this end there should be a music namespace containing the simplest yet most helpful functions needed to make this work.

Furthermore, we'll need to define an easy-to-understand DSL for kids and teachers to use for specifying notes.

Finally, it would be wonderful if such interactions could be optionally allowed to run in the background (in a similar way to the display.animate method).

ntoll commented 8 years ago

Initial thoughts on DSL.

From a music teacher's perspective, the simplest thing we can do is create a Python list containing items that specify the following pieces of information:

To provide an example of how this may look: 'A1:16' could represent the note 'A' in octave 1 that lasts for 16 ticks (where a tick is some arbitrary length of time defined by a tempo setting in BPM - see below).

Furthermore, we could short-cut octave and duration so the following list contains four notes all in octave 1 with a duration of 4 ticks: ['C1:4', 'D', 'E', 'F', ]

In other words, the octave and duration are states that carry over to the subsequent notes until they are re-specified, perhaps like this: ['C1:4', 'D:2', 'D', 'E:8']

Finally, the note names should be case insensitive so 'c' and 'C' are equivalent.

Setting the tempo could be set by a function perhaps looking like this:

def set_tempo(ticks, bpm):
    """
    Sets the approximate tempo for playback.

    A number of ticks (integer) are to be played at a certain frequency per minute 
    (expressed as the more familiar BPM - beats per minute).
    """"
    ...

Ergo, set_tempo(4, 120) ( a suggested default) would feel like setting a crotchet (that lasts 4 ticks) at 120 bpm (a brisk march tempo). As a result, 16 ticks = semibreve, 8=minim, 2=quaver and 1 a semiquaver allowing us the maximum rhythmic definition to be expressed in terms of semiquavers.

We could also have a get_tempo() function that returns a tuple of (ticks, bpm).

The simplest functionality we'd need to provide to play things could be expressed as four functions:

All of the above could live under the microbit.music namespace.

Thoughts..? Could this be implemented in the C++ layer..? As @whaleygeek said on the mailing list - this could be a total game-changer for MicroPython on the micro:bit... everyone would want to use Python simply because you can make things go "bloop" with little effort. ;-)

ntoll commented 8 years ago

Actually, perhaps the play and pitch functions should also take the wait keyword so they're non-blocking.

matthewelse commented 8 years ago

I've defined the API in a wiki page: https://github.com/dpgeorge/microbit-micropython/wiki/music-API

ntoll commented 8 years ago

OK... I think we should rename play to the better (and more accurately named) name note. I'll update the wiki page to reflect this change.

ntoll commented 8 years ago

OK... I've added and revised a bunch of information to the wiki so it's more like a formal specification.

matthewelse commented 8 years ago

C++ code for music is coming along nicely - I still need to sort out functionality like persistence between notes, which might lead me to making a Tune class again, however for the time being it supports play_note and play_notes, so it should be easy to play the example @ntoll created previously

dpgeorge commented 8 years ago

Matthew: did you see the wonderful music API that Nicholas defined? At: https://github.com/dpgeorge/microbit-micropython/wiki/music-API

On Fri, Sep 25, 2015 at 8:27 PM, Matthew Else notifications@github.com wrote:

C++ code for music is coming along nicely - I still need to sort out functionality like persistence between notes, which might lead me to making a Tune class again, however for the time being it supports play_note and play_notes, so it should be easy to play the example @ntoll https://github.com/ntoll created previously

— Reply to this email directly or view it on GitHub https://github.com/dpgeorge/microbit-micropython/issues/31#issuecomment-143331409 .

matthewelse commented 8 years ago

yeah I did :+1: for some reason, I missed the bit where it said tune instead of play_notes - I'm now tidying up the implementation so it's more like the API.

dpgeorge commented 8 years ago

@ntoll I think the API is very clean, simple and just the right amount of functionality.

I'm just a bit confused by set_tempo(): if ticks=4 and bpm=120 does that mean it runs at 4*120=480 Hz?

It's nice how the functions are all stateless (in that there is no memory between functions, apart from what's going on with the pin) (actually, set_tempo is not stateless...). But that does mean you need to pass the pin to each function. Would it be better to make a music object, which is constructed with a pin, and has note/pitch/tune/stop methods? Then the tempo could be stored in this object as well. And if we get PWM working on independent channels/pins (might be possible) then you could make multiple music objects, each with a different tempo and pin?

matthewelse commented 8 years ago

I was thinking this myself. I think that it makes the most sense to have a similar API to what I originally created in the Tune class, with similar methods to those described in the wiki page.

Personally, I think that if we're using variables inside modules to retain state between function calls, then we're doing it wrong, so a class makes more sense...

ntoll commented 8 years ago

OK... so classes and OO will be beyond most 11yo especially when it comes to something as abstract as music (FYI, I think the Image class is beautifully simple and an appropriate first step in this direction).

I know from experience with teaching adults (who often don't get OO) that they'll simply be the victim of the following sort of movement of thought, "just do this, then this, now do this thing and it'll just work.... oh what do you mean it doesn't work? Well, I dunno..?". My point is that to use OO you need to understand it and that takes a significant amount of abstract thought that many adults - let alone 11yo - have a problem with. With my teacher and musician hats on, I'd strongly oppose the introduction of OO via a Tune class in this module. It's too "magic" for new programmers.

Making the API purely functional is something that an average 11yo will understand: a function call is how you get Python to do some particular thing, "I can even make one of my own". By this stage of their learning, the curriculum states they should be introduced to such a notion: at KS3 a student should be taught to "design and develop modular programs that use procedures or functions" (ref: https://www.gov.uk/government/publications/national-curriculum-in-england-computing-programmes-of-study). FYI Key Stage 3 = 11-13yo. OOP doesn't even get a mention in KS4, IIRC it's only introduced at post-16 A-level - the step just before university.

This is about trade-offs, and given the intention of the micro:bit is a device for 11yo I think we have some leeway when it comes to design of the API. For instance, I started with the Tune class and created something that another developer would understand in seconds. But I'd fallen victim to ignoring my users (11yo kids) - hence restarting the API and throwing away OO.

Yes, tempo, octave and tick state stored in the module is a shonky thing to do - but it means kids get to use code that is easy for them to reason about. Also, a module is an object, so one could argue that keeping state therein is not so nasty as it first appears.

:-)

It's like embedding the hex file within a div in the web-based editor. You probably shouldn't do that, but if a kid clicks on "View Source" they get to see something interesting that encourages more exploration. Put simply, my heuristic is to put kids first.

No matter how "correct" or "elegant" we make our API, if we do it in such a way that kids won't understand then we're wasting our time.

@dpgeorge regarding the setting of tempo - the first argument is the number of ticks in each beat (this defaults to 4 - so one beat could be subdivided into 4; if 1 beat = a crotchet then 1 tick = a semiquaver), the second argument is the number of such beats that should happen in a minute - 120 being an allegro moderato (moderately fast).

Ergo, to work out the length of a tick in milliseconds is very simple arithmetic: 60000/bpm/ticks-per-beat. For the default values that's 60000/120/4 = 125 milliseconds or 1 beat = 500 milliseconds.

Therefore, given the default tempo setting, music.note(P0, 'A3:4') is equivalent to music.pitch(P0, 440, 500) - a standard concert A for half a second.

Does this make sense? I'm trying to couch this in terms a music teacher or child would understand - apologies for my confusing explanation. :-/

On the subject of specifying pins: I believe this is important since the pin is conceptually similar to the "part" in a musical sense - this thing needs to play these notes. In music we always specify the part - just take a look at an orchestral score and you'll see what I mean... (viz. http://imslp.org/wiki/File:PMLP03875-Beethoven-Op073FSeu.pdf). I'd like us to be conceptually consistent between the musical and programming worlds, it simply makes it easier to transfer skills across domains.

Please feel free to push back... although I believe there needs to be exceptional reasons for a Tune class - reasons that benefit ease of learning rather than some notion of software engineering "correctness". ;-)

ntoll commented 8 years ago

I've updated the wiki page to explain the setting of tempo more clearly. Apologies for the confusion.

dpgeorge commented 8 years ago

Good points Nicholas. I agree with you.

Note that we do have OO everywhere (eg display.print, pin0.set_pixel) but actually the kids can consider it all just function calls with names that contain a dot. There are no constructors (except image).

So keeping with this scheme with the music module is sensible. (The "music" entity could even be a precreated instance of a Music class, just like display, but that's an implementation issue.)

The only downside of having it functional but with state for the tempo is that you can't create multiple music streams/parts (on separate pins) with different tempos. But you can still do multiple parts with the same tempo so I think that's powerful enough.

ntoll commented 8 years ago

Exactly - the only thing we currently require kids to instantiate is an Image class and that's like saying "I'm making a picture" so relatively easy for an 11yo to grok because there's something to potentially look at as a result.

Regarding different tempi on different pins - this is very very unusual.

Only in the 20th century did anyone actually try it out (IIRC the originator was American composer Charles Ives - I wrote my undergraduate dissertation on his music; I love it, although it's definitely an acquired taste).

Put simply, if you're wanting to do concurrently multi-tempo parts then you ought not to use a micro:bit and probably have enough nous to program something more sophisticated than our little functional API.

:-)

Does this make sense?

ntoll commented 8 years ago

Hi @matthewelse, how's this work going? You mentioned you'd made some progress!

The last thing I want to do is hassle you, but I thought you might be interested to know that someone from the BBC got in touch about the launch event when the kids actually get the devices and the possibility of using a micro:bit to make music.

Obviously, I'd like to respond with some useful information and feedback on the state of progress. ;-)

matthewelse commented 8 years ago

Hi Nicholas

I've had a bit of a busy week getting ready for uni, but the functionality is all implemented, and I'm confident I can sort out the structural stuff before this weekend. On Fri, 2 Oct 2015 at 10:14, Nicholas Tollervey notifications@github.com wrote:

Hi @matthewelse https://github.com/matthewelse, how's this work going? You mentioned you'd made some progress!

The last thing I want to do is hassle you, but I thought you might be interested to know that someone from the BBC got in touch about the launch event when the kids actually get the devices and the possibility of using a micro:bit to make music.

Obviously, I'd like to respond with some useful information and feedback on the state of progress. ;-)

— Reply to this email directly or view it on GitHub https://github.com/dpgeorge/microbit-micropython/issues/31#issuecomment-144967874 .

ntoll commented 8 years ago

Great stuff... looking forward to the PR. :-)

matthewelse commented 8 years ago

See #39 for the progress I've made

dpgeorge commented 8 years ago

Music module is now merged and mostly works. There seem to be some bugs with certain notes (eg music.tune(pin0, ['a', 'b', 'c', 'd']) seems wrong) and the default octave doesn't sound like the 4th one.

matthewelse commented 8 years ago

@dpgeorge I've just had a look at this: if you do a, b, c, d then it will play all of the default octaves (i.e. a4, b4, c4, d4), and as it turns out if you want an ascending scale from a, you need to do:

music.tune(pin0, ['a', 'b', 'c5', 'd5'])
# or
music.tune(pin0, ['a', 'b', 'c5', 'd'])
matthewelse commented 8 years ago

There was a bug that has been fixed in #40. I'll remove my assignment from this issue since this is now functionally complete.

dpgeorge commented 8 years ago

if you want an ascending scale from a, you need to do: ...

Yes, I see. This shows my complete lack of understanding of music theory!

dpgeorge commented 8 years ago

Music module works nicely now. What remains is to implement non-blocking mode.

matthewelse commented 8 years ago

Is there the necessary functionality to do this without digging too far into the DAL?

matthewelse commented 8 years ago

I feel like if there were a function along the lines of 'schedule callback after interval' that would solve more or less all of our problems.

dpgeorge commented 8 years ago

I feel like if there were a function along the lines of 'schedule callback after interval' that would solve more or less all of our problems.

Yes, if we just had this hook then we could do all our own background scheduling stuff. Then we can make sure we get the semantics correct (including edge cases etc).

I think the best way is to make our own MicroBitComponent and schedule it to be executed on each system tick using:

uBit.addSystemComponent(&mpSystemTick);
matthewelse commented 8 years ago

So we could make a class with a name along the lines of MicroBitScheduler, with things like 'schedule callback' etc.

dpgeorge commented 8 years ago

I don't think we want Python callbacks. Just low-level ones to implement non-blocking features of music/display/etc.

matthewelse commented 8 years ago

Sorry, I meant a singleton C++ class could be used as an overall scheduler for these things

dpgeorge commented 8 years ago

Yes. But we need to be resource conscious and only implement what's really necessary, not a full-blown generic scheduler.

matthewelse commented 8 years ago

In terms of getting async working, I think that the mbed Timeout class might do what we need to do

matthewelse commented 8 years ago

@dpgeorge @ntoll I may start having a look at non-blocking music this afternoon, since I have some free time. I'll let you know if I get anywhere with it

ntoll commented 8 years ago

OK... I've made a couple of small changes to the code in a branch. Let me push that into a PR before you start.

ntoll commented 8 years ago

See #44 for my minor updates. I hope to add some tunes this afternoon. ;-)

matthewelse commented 8 years ago

Just realised - I almost forgot about the async work for this module - I'm going to try and have a look at this in the next week I think.

ntoll commented 8 years ago

Great stuff! :-)

dpgeorge commented 8 years ago

@matthewelse any update on async music? One should be able to put a simple hook in main.cpp:ticker() to schedule the next note.

matthewelse commented 8 years ago

@dpgeorge I've been quite busy with uni work over the last few weeks unfortunately - I actually made quite a bit of progress with it before I forgot about it for a while, so I might go back and have a look at it once I've finished off the stuff I have to do at the moment. Hopefully I should have some free time tomorrow, depending on how things go.

dpgeorge commented 8 years ago

@matthewelse ok. If you just want to post what you already have and let someone else take it up, that would be fine.

ntoll commented 8 years ago

@matthewelse how's this going..? :-)

dpgeorge commented 8 years ago

I am now working on async music.

dpgeorge commented 8 years ago

Async music implemented in 0189136af557bc2c8514935f7351bcaafe32c47f

Remind me again why it's a music object within the microbit module, and not a music module in its own right?

ntoll commented 8 years ago

@dpgeorge I don't remember any reason why it's in the microbit module. To be honest I'm not sure it should be since the microbit module is all about access to the device itself. A music module is something that builds on top of that.

I'm in a long train journey to Birmingham late this afternoon. I'll have a play with this code while on the journey. ;-)

matthewelse commented 8 years ago

Thanks @dpgeorge - sorry about that I haven't had much time to look at this unfortunately.

dpgeorge commented 8 years ago

@matthewelse no problem :)

I think we should make it a separate music module. Now that I revisited this code after a few weeks not using it, my first instinct was that I should do "import music", and then I was a bit surprised to see that music was a singleton object within microbit module.

If we have a neopixel driver in a neopixel module, then it makes sense that music is a separate module as well. Music is slightly advanced because it requires you to attach a speaker to pins, so doing "import music" is not going to be a hurdle.

dpgeorge commented 8 years ago

I made "music" a module in a9ccc4812d23829898915b9b92ea56d756517f35. You now need to import it before using it (import music).

The title of this issue is anyway "add music module", and in fact the docs that have been written assumed that music was a module, so I think it's clear that it should indeed be a module :)

dpgeorge commented 8 years ago

There may be things to tweak, bugs to fix and more tunes to add, but I think we can close this issue for now because we do indeed have a very nice music module.