SkillAgentSearch skills...

Mmml

Micro Music Macro Language - An MML Implementation for 1-Bit Music on AVR Microcontrollers

Install / Use

/learn @protodomemusic/Mmml
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

μMML - Micro Music Macro Language

An MML implementation for AVR microcontrollers (and other platforms, but largely 1-bit focused still).

This little project was built to facilitate simple composing of 1-bit music for AVR microcontrollers using a derivation of Music Macro Language (MML). You can then, with the most basic of components, make cool embedded albums that run off a single coin cell battery.

Scruss has written an excellent, incredibly comprehensive, guide to programming and building your own 1-bit music boxes using μMML, which you can find HERE. If you're new to AVR programming, this is where you should really start.

You can also find a small tutorial HERE by garvalf (under the MMML heading).

I hope you have fun writing tiny music! If you have any questions/suggestions/corrections contact me at: hello@protodome.com

10th May 2020

Removed the old AVR specific compiler and updated the AVR player to work with the new format. Now there are three possible build targets:

  1. -t avr for AVR microcontroller. Creates an .h include file for the mmml-avr-player.c program.
  2. -t gb for Game Boy. Creates a .c file for the mmml-gb-player.c program (not included just yet).
  3. -t data for desktop/DOS. Creates an .mmmldata file for the desktop synthesiser. Also read by the DOS player (not included just yet).

As the compiler is now centralised, it will be updated in a single location, supporting all future build platforms.

9th May 2020

Fixed the compiler on Windows. Additionally added some really early compiler support for the Game Boy μMML player (which is coming later).

As there are multiple build targets, you will have to run the compiler like this:

$ ./compiler -f FILENAME.mmml -t data

This will set the build (t)arget to .mmmldata file. (If you want Game Boy, you'll need to do -t gb, but that will produce a useless C file right now without the player.)

19th April 2020

I've added a bare-bones wave synthesizer that interprets .mmmldata files built by a new desktop compiler. The synthesizer requires two flags, an input file '-f' and a duration in seconds '-s'. As μMML currently has no way to determine the end of a track (officially anyway, you may notice that 0xFE is a 'track end' flag; it's a pretty flaky system I used for the 4000AD physicals), you currently have to tell the synthesizer how many seconds you'd like it to run for. So, building and running the new features might looks something like this:

Build both programs: $ gcc mmml-desktop-compiler.c -o compiler

$ gcc mmml-desktop-synthesizer.c -o synthesizer

Run the compiler first... $ ./compiler -f FILENAME.mmml -t data

...then build the output file. $ ./synthesizer -f output.mmmldata -s 60

Which will create a 60 second long wave file.

Additionally, I'm moving all the compilation features into a single compiler, so that it can build desktop and avr sources. There's a DOS player in the works, so I'm trying not to have multiple forks of the compiler, especially if the core mmml engine is expanded. As a heads-up, to support this planned functionality, the compiler will now require flags to declare input files (-f). This means that the code in the 'avr' folder is destined to be replaced at some point and, as such, I will not be updating the compiler there any longer.

There's also been a fix for 64-bit Linux systems where error 14 fired off erroneously. This fix is on the desktop compiler only for now.

How To Use

Note: The compiler is very bare-bones, stubborn and inflexible at the moment. It is in desperate need of complete refactoring. It does work however, and that's all it needed to do to write my 1-bit album. So, with that in mind, read on and, if you like this project and want to help out, I would be honestly thrilled.

This guide assumes you're using OSX or Linux. It definitely works on Windows so, if you know what you're doing, it should be simple.

There are two required components to this project: the mmml.c player for your chosen AVR microcontroller and the mmml-compiler.c program for your chosen OS. The mmml.c player is built for AVR microcontrollers clocked at 8MHz (such as the Attiny85, or Atmega168) but, with a little tweaking, you should easily be able to adapt for other platforms. The mmml.c player requires prerequisite use of the mmml-compiler.c program to create a musicdata.h data file (where your song data is stored).

The mmml-compiler.c program will convert .txt files to create the required bytecode tucked up in the musicdata.h include file. You will need to put the resultant musicdata.h file in the same directory as the mmml.c file so that both can be easily found when flashing to the chip.

Building The Compiler

To run mmml-compiler.c, you must compile with your system. For example, on Linux / OSX:

$ gcc mmml-compiler.c -o mmml-compiler

This will create an output file named 'mmml-compiler' (or whatever is entered after the -o flag), then run the output program from the terminal like so:

$ ./mmml-compiler

On Linux you will need to install the 'GCC' package if it is not already:

$ sudo apt-get install gcc

On OSX, you will need to install 'Command Line Tools'. In Mac OS 10.9 (and later), this can be achieved by using the following command:

$ xcode-select --install

Uploading To Microcontroller

To program your microcontroller is a little more involved, but not hard! You'll need the AVRDUDE software and you might want to follow this Hackaday guide (which explains things better than I could).

What Does The μMML Player Do?

The mmml.c program is a four channel routine: three channels of melodic, 1-bit pulse waves and a simple, percussive PWM sampler. Each channel is labelled A-D respectively, and all are mixed via pulse interleaving (rapidly switching between channels in sequence). The composer has the choice (before compiling) to replace the sampler (channel D) with a percussive noise generator instead, which (currently) cannot be dynamically toggled in software.

There are eight pulse waves available: 50%, 25%, 12.5%, 6.25%, 3.125%, 1.5625%, 0.78125% and 0.390625%. The fact there are only eight selectable widths is kind of arbitrary (the routine can generate loads of them) however these eight been selected due to both simplicity of implementation (the waveform peak is defined by enumerating the frequency, divided by powers of two: waveform = frequency >> n;) and because they are the most timbrally unique. Remember, at thinner widths (6.25% to 0.39065%), there will be a change in volume rather than timbre.

Let's Get Technical

The compiled bytecode (read from the musicdata.h file - generated by the mmml-compiler.c program) is structured as follows:

        ______BYTE_____
       |               |
BITS : [0000]     [0000]
FUNC : [COMMAND]  [VALUE]

Each byte is split into two 'nibbles': four bit values with a possible range of 0-16. Each nibble requires a second, defining the value of the command. This is not entirely abstracted from in the human readable language, but it is key to understanding why some variables are limited to a maximum of 16 states. The language is structured so that the most frequent commands required are represented by the smallest data type interpreted by mmml.c. In a few cases, this structure is appended with an additional byte, as below:

        ______BYTE_____          __BYTE__
       |               |        |        |
BITS : [0000]     [0000]        [00000000]
FUNC : [COMMAND]  [IDENTIFIER]  [VALUE]

This behaviour is required by the 'Function' command, where there is always a trailing byte (technically a two-byte value). When higher precision is required, the nibble datatype, capable of representing numbers from 0 - 15, is not sufficient, thus the Function command uses the structure of a general command as a generic 'flag', capable of specifying sixteen additional functions and indicating whether the next byte in data should be interpreted as a new command, or an extension of the previous.

There are four main chunks of data for each channel, plus an additional block of data for each macro (channel agnostic sequences, referable in a channel's 'main chunk' of data). These are stored contiguously in a single, one-dimensional unsigned char array called data. The structure of this array is stored in a companion, unsigned int array called data_index which, as the name suggests, holds an index of where each chunk of data begins. The first four chunks are always channels A, B, C and D respectively, then macros 1, 2, 3... etc. It is unhelpful to imagine a specific maximum length of events for individual channels, it is dependent on the global size of all channels and macros combined, where any data beyond 65535 cannot be indexed (due to limitations of the declared array datatype).

A table listing all possible commands in the core music data read by the mmml.c routine, alongside their evocation values can be found below. The 'value' field lists the first nibble in each byte; the program then expects a further trailing number between 0-15. In the case of the 'function' command (1111, or 0xF), this trailing value must be one of those listed in the lower table. mmml.c then requires an additional byte, allowing values from 0-255.

--------------------------------  -------------------------------------
| µMML | BYTECODE | COMMAND    |  | µMML      | BYTECODE | COMMAND    |
--------------------------------  -------------------------------------
| r    | 0000     | rest       |  | g         | 1000     | note - g   |
| c    | 0001     | note - c   |  | g+        | 1001     | note - g#  
View on GitHub
GitHub Stars115
CategoryDevelopment
Updated1mo ago
Forks6

Languages

C

Security Score

95/100

Audited on Feb 16, 2026

No findings