Closed ntoll closed 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:
play(pin, note)
- takes a pin and a string expressing pitch and duration, e.g.: 'C1:8'
and plays it at the pre-defined tempo .pitch(pin, freq, millis)
- takes an integer representation of a frequency of a pitch (e.g. 440 - concert A) and a duration in milliseconds. This would allow us to create interesting sweeping effects and non-diatonic notes (i.e. not part of the scale system) for arbitrary periods of time. It'd be a great way to make a siren for example.tune(pin, notes, wait=True, loop=False)
- takes a pin and a list of string based note definitions (a la description above). The optional loop
and wait
keywords would work just like the animate
function in the display
module.stop(pin)
- stops any generation of sound on the given pin.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. ;-)
Actually, perhaps the play
and pitch
functions should also take the wait
keyword so they're non-blocking.
I've defined the API in a wiki page: https://github.com/dpgeorge/microbit-micropython/wiki/music-API
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.
OK... I've added and revised a bunch of information to the wiki so it's more like a formal specification.
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
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 .
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.
@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?
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...
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". ;-)
I've updated the wiki page to explain the setting of tempo more clearly. Apologies for the confusion.
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.
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?
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. ;-)
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 .
Great stuff... looking forward to the PR. :-)
See #39 for the progress I've made
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.
@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'])
There was a bug that has been fixed in #40. I'll remove my assignment from this issue since this is now functionally complete.
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!
Music module works nicely now. What remains is to implement non-blocking mode.
Is there the necessary functionality to do this without digging too far into the DAL?
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.
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);
So we could make a class with a name along the lines of MicroBitScheduler, with things like 'schedule callback' etc.
I don't think we want Python callbacks. Just low-level ones to implement non-blocking features of music/display/etc.
Sorry, I meant a singleton C++ class could be used as an overall scheduler for these things
Yes. But we need to be resource conscious and only implement what's really necessary, not a full-blown generic scheduler.
In terms of getting async working, I think that the mbed Timeout class might do what we need to do
@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
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.
See #44 for my minor updates. I hope to add some tunes this afternoon. ;-)
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.
Great stuff! :-)
@matthewelse any update on async music? One should be able to put a simple hook in main.cpp:ticker() to schedule the next note.
@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.
@matthewelse ok. If you just want to post what you already have and let someone else take it up, that would be fine.
@matthewelse how's this going..? :-)
I am now working on async music.
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?
@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. ;-)
Thanks @dpgeorge - sorry about that I haven't had much time to look at this unfortunately.
@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.
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 :)
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.
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).