Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mmml Player as Arduino Sketch #4

Closed
pdr0663 opened this issue Sep 29, 2021 · 15 comments
Closed

mmml Player as Arduino Sketch #4

pdr0663 opened this issue Sep 29, 2021 · 15 comments

Comments

@pdr0663
Copy link

pdr0663 commented Sep 29, 2021

I have implemented the code as an Arduino sketch but I have an issue.

The playback seems to be very fast. I'm using an Arduino Nano, and I believe the clock speed is 16MHz, not 8 MHz. Looking at the timing parts in your code, there is the following:

  // main timer variables
  unsigned int   tick_counter    = 0,
                 tick_speed      = 1024; // default tempo

and

        /* Waste the same number of clock cycles as it takes to process the above to
         * prevent the pitch from changing when the sampler isn't playing. */
        for(unsigned char i=0; i<8; i++)
          asm("nop;nop;");

I've tried tweaking these values without success. Can I assume then that the code is inherently tuned to 8MHz clock speed, or is the code easily tunable to a 16MHz clock?

Is there anything inherently problematic using the Arduino bootloader firmware + mmml player sketch?

Paul

@protodomemusic
Copy link
Owner

Firstly, thank you very much for trying out the code!

There's actually an interesting issue here. On the face of it, it's a simple fix - just add the following at the top of the 'void loop()' function:

// adjustment for different clock speeds
// 28 isn't quite right, but you can manually tune this by changing the number of loops, or adding more 'nops'

for(unsigned char i = 0; i < 28; i++)
{
	asm("nop;");
	PORTB = 0;
}

The issue is, this will halve the output volume as now the output is just zero for most of the time. You'll also probably hear more audio glitches (it's definitely more 'clicky'). If you remove the PORTB = 0, the last channel expressed on the output will be super loud. In this case, it makes the sampler double the volume. The reason you can't just change the clock speed is the section from lines 202-231: I've carefully interleaved the output code so that each channel is nearly perfectly balanced (it's done this way for speed of code execution). This manual interleaving was done at 8MHz explicitly.

Because we have a little more headroom at 16MHz, I've just tried moving the mixing/slowdown code into something like this:

// slowdown
for(unsigned char i = 0; i < 28; i++)
	asm("nop;");

// mixer
mixer++;
PORTB = out[mixer % 4];

... And it's sounding pretty rough - bright and glassy - like the mixing frequency is audible or something. Theoretically, it should be evenly balanced, so I'm definitely missing something obvious here.

The solution might have to clock it properly with a hardware timer but, for now, that first bit of code should be good enough to get it working at 16MHz. I'll leave this issue open and see if I can come up with anything better.

@pdr0663
Copy link
Author

pdr0663 commented Sep 29, 2021

Thanks for your detailed reply.

Congratulations on the software, it's a fascinating piece of work. From what I can tell, it includes:

  • The compiler
  • An interpreter for the compiled music ( a feat in itself)
  • On the fly generation of frequency pulses for 3 voices
  • sampled music for another voice

It's a fantastic piece of software. It'd be great to implement it with timers, as the timing would be easier to resolve, and perhaps the software could do something else in between timer ticks (my interest for embedded devices).

I read the article quoted in your piece here:

https://scruss.com/blog/2020/04/02/protodomes-wonderful-chiptunes-how-to-play-them-on-your-own-attiny85-chips/

for anyone who hasn't seen it. It's very enlightening.

Paul

@pdr0663
Copy link
Author

pdr0663 commented Sep 29, 2021

By the way, the article does not mention another famous 1-bit implementation, the Apple ][. Much of this work would already have been done by about 1980 by game programmers.

@pdr0663
Copy link
Author

pdr0663 commented Sep 30, 2021

I'm trying to get my head around the micro-timing in the software. It seems that the period is the time between PORTB= statements in the code where the pulse calculations happen. The location of these statements in your code determines the period between each it seems. Every tick_speed (1024) times through the main loop, you do the music interpretation stuff. This would be a stutter in the output but it seems it's not noticeable. This seems to be PPM, am I right?

@protodomemusic
Copy link
Owner

Yeah, I'm an idiot. After I sent that yesterday I realised that there's a much simpler way to get this working nicely without messing with the timing of the mixer.

Just change this line: tick_speed = buffer3 << 4; to this: tick_speed = buffer3 * 28;
And this line: buffer4 = pgm_read_word(&note[buffer1]); to this: buffer4 = pgm_read_word(&note[buffer1]) << 1;

Because we're doubling the frequency, we just need to double the note/tempo values; it doesn't matter at all if the speed of code execution changes, as long as it stays proportionally the same. As an aside, you can also do the mixing nicely on an interrupt:

// clocked mixing
unsigned char  mixer = 0;

void setup()
{
	for(unsigned char i=0; i<CHANNELS; i++)
	{
		data_pointer[i] = pgm_read_byte(&data[(i*2)]) << 8;
		data_pointer[i] = data_pointer[i] | pgm_read_byte(&data[(i*2)+1]);
		frequency[i]    = 255; // random frequency (won't ever be sounded)
		volume[i]       = 1;   // default volume : 50% pulse wave
		octave[i]       = 3;   // default octave : o3
	}

	// initialise output pin
	DDRB = 0b00000001 << OUTPUT;

	// timer 2 init
	TCCR2A = _BV(WGM21);
	TIMSK2 = _BV(OCIE2A);
	TIFR2  = 0;
	TCCR2B = _BV(CS21);
	
	// the lower the number, the faster the mixing
	OCR2A = 20;
}

// do the mixing on interrupt
ISR(TIMER2_COMPA_vect)
{
	mixer++;
	PORTB = out[mixer % 4];
}

To answer your other questions (in reverse order):

  1. It doesn't stutter because it: 1. executes relatively quickly and 2. the output isn't changing over this period of time. I'm actually exploiting the duration of the code execution to make the sampler channel louder compared to the oscillators. Because the last output is output[3] (the location where the output data for the sampler is), it spends much longer expressing that output than any of the others, which means it becomes perceptually louder. If you swap around those PORTB = output[x]; statements, you'll see that whichever you put last (before the sequencing code), will be loudest. If, in the MMML code, you put a huge series of contiguous commands that caused the sequencing code to repeatedly loop, you'd probably notice a drop in pitch/mixing speed - but it just practically will never happen.

  2. Oh yeah, there's some absolutely amazing (much better) pre-existing stuff out there - even for Arduino. Check out this: http://randomflux.info/1bit/viewtopic.php?id=125

  3. You could definitely do the whole thing with timers (the routines linked above do this), and I might do it properly later, but 1-bit probably isn't the most effective solution to audio on the ATmega328. Or anything actually: because the rate of update has to be so high, you end up doing nothing else but processing audio. If you did three software channels merged into a 8KHz PWM output, you'd have buckets of time for other stuff. You wouldn't get that distinctive bright, crunchy, wobbly 1-bit aesthetic though...

Also, thank you very much for the kind words!

@pdr0663
Copy link
Author

pdr0663 commented Oct 5, 2021

Thanks for the mod. I tried it and it works perfectly! The rendition of "till there was you" is hilarious.

@pdr0663
Copy link
Author

pdr0663 commented Oct 5, 2021

I have written a small MMML file for some tones we use on our products. I was hoping to simulate on a micro, using your mmml player. Here it is:

%===================================================================%
% TITLE      : 'sting'
% COMPOSER   : Unknown
% PROGRAMMER : Paul Riley
% NOTES      : 
%              
% DATE       : 5th October 2021
%===================================================================%

%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL A %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 v6 r4

g4 > g4 f#4 d4 < b1


%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL B %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@


%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL C %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@


%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL D %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@

I don't get any sound at all. Can you spot an error in the file?

Thanks,

Paul

@protodomemusic
Copy link
Owner

Yeah, it's just badly tested code on my part. You need a byte of something in-between the channel declaration commands. It'll compile, but the player will have a meltdown. To fix this, the compiler should just thrown in a hidden byte if a channel is left undeclared/with no data inside.

Also, don't forget to put a tempo in! I can't remember if the AVR one has a pre-initialised tempo or not.

%===================================================================%
% TITLE : 'sting'
% COMPOSER : Unknown
% PROGRAMMER : Paul Riley
% NOTES :
%
% DATE : 5th October 2021
%===================================================================%

%--------------% CHANNEL A %--------------%

@ t35 o4 v6 r4

g4 > g4 f#4 d4 < b1

%--------------% CHANNEL B %--------------%

@ r4

%--------------% CHANNEL C %--------------%

@ r4

%--------------% CHANNEL D %--------------%

@ r4

%--------------%  MACROS   %--------------%

@ r4

You don't HAVE to put a macro in (I think it'll play without), but I don't think I ever tested this without some kind of data declared down there. It's always safer to declare one anyway and stick a byte of whatever in there.

@pdr0663
Copy link
Author

pdr0663 commented Oct 12, 2021

Any reason why this won;t compile?

%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL A %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 t50 v6

  g4
  v3g4
  >v6f#4
  &v3f#4
  
  r1
  
  r1
  
%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL B %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 v6

  r4
  >g4
  &v3g4
  <v6d4

  &v3d4
  r4
  r4
  r4

  r1
  
%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL C %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 v6

  r1

  b4
  &b4
  &v3b4
  &v3b4

  r1

%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL D %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 v6

% Macros

@ r4

@protodomemusic
Copy link
Owner

Oh of course, you just need to add an r4 or whatever to channel D, like this:

%-~-~-~-~-~-~-~-~-~-~-~-~-~-% CHANNEL D %-~-~-~-~-~-~-~-~-~-~-~-~-~-%

@ o4 v6 r4

You're the first person to really stress-test this, haha. Without a duration command to prompt movement to the next channel's data, the player will hit that o4 v6 then get stuck in an infinite loop. Like before, I should really add a bit of code to check for exceptions like this - probably at the initial mmml compiler level (keeps it simple then).

@pdr0663 pdr0663 closed this as completed Dec 15, 2021
@farvardin
Copy link

hey @pdr0663 do you have the code for this mmml player on arduino somewhere? I'd like to try it...

@pdr0663
Copy link
Author

pdr0663 commented Nov 2, 2023 via email

@pdr0663
Copy link
Author

pdr0663 commented Nov 2, 2023 via email

@pdr0663
Copy link
Author

pdr0663 commented Nov 2, 2023 via email

@farvardin
Copy link

farvardin commented Nov 2, 2023

Here it is. I included the sample mmml file, it's a hoot! You'll need to check the output pin. Paul Paul Riley Mo: +61 (0)411 781 394 Email: @.***

@pdr0663
hello, thank you but there is no attachment, maybe because it was sent from email? Do you have both the .ino file and eventually the mmml file (even though I've already several mmml samples)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants