m5stack / M5Unified

Unified library for M5Stack series
MIT License
324 stars 58 forks source link

How to make audio tone from Core2 speaker #12

Closed patfelst closed 2 years ago

patfelst commented 2 years ago

Hi there, I would like to create a simple tone at a specific frequency from the Core2 speaker. I saw M5 stack API has this:

  M5.Speaker.begin(); //Initialize the speaker
  M5.Speaker.tone(661, 1000);    //Set the speaker to tone at 661Hz for 1000ms

But it is not in M5Unified as far as I can tell. Or do I need to do a full I2S implementation like in this example https://github.com/m5stack/M5-ProductExampleCodes/blob/master/Core/M5Core2/Arduino/speak/speak.ino

Thanks for your help!

lovyan03 commented 2 years ago

@patfelst Thanks for your report . Audio support is currently not available. (There are plans to implement it.) I apologize for the wait, but it is a feature I am eagerly awaiting.

We plan to make it compatible with multiple devices. Therefore, it is not sufficient to simply copy the Core2 implementation.

patfelst commented 2 years ago

ok thanks for that, I look forward to it being implemented. I will try a full I2S example.

lovyan03 commented 2 years ago

@patfelst Thank you for your patience. Today's release of M5Unified v0.0.5 adds the M5.Speaker . You can now use the tone function as well. Please try it out !

patfelst commented 2 years ago

Excellent thank you I will try it out soon!

patfelst commented 2 years ago

I just tried the tone function and it works great. The examples are well commented - thanks. The virtual channels is a nice addition too. I may try wav file later.

I couldn't work out what this option for tone was:

// tone data (8bit unsigned wav)
  const uint8_t wavdata[64] = { 132,138,143,154,151,139,138,140,144,147,147,147,151,159,184,194,203,222,228,227,210,202,197,181,172,169,177,178,172,151,141,131,107,96,87,77,73,66,42,28,17,10,15,25,55,68,76,82,80,74,61,66,79,107,109,103,81,73,86,94,99,112,121,129 };

  /// Using a single wave of data, you can change the tone.
  M5.Speaker.tone(261.626, 1000, 1, true, wavdata, sizeof(wavdata));
  delay(200);
  M5.Speaker.tone(329.628, 1000, 2, true, wavdata, sizeof(wavdata));
  delay(200);
  M5.Speaker.tone(391.995, 1000, 3, true, wavdata, sizeof(wavdata));
  delay(200);
lovyan03 commented 2 years ago

The default behavior of the tone function is to output a square wave, but you can also specify any waveform data to play a sound. The 64-byte array in that example is a small wav data for a single amplitude created by a software synthesizer.

  M5.Speaker.tone( 261.626     // tone frequency (Hz)
                 , 1000        // tone duration (msec)
                 , 1           // virtual channel number. (0~7), (default = automatically selected)
                 , true        // true=start a new output without waiting for the current one to finish.
                 , wav         // Single amplitude audio data. 8bit unsigned wav.
                 , sizeof(wav) // size of wav_data.
                 , false       // true=data is stereo / false=data is mono.
                 );
patfelst commented 2 years ago

This is great, so I could define a sine or sawtooth or other waveform! I might have to study your source code to see if I fully understand, but what is the assumed sampling rate, or period of the waveform data? Or is the waveform data applied evenly over the tone duration?

lovyan03 commented 2 years ago

The sampling rate and repeat count is calculated based on the length of the wav data.

https://github.com/m5stack/M5Unified/blob/0.0.5/src/utility/Speaker_Class.cpp#L409-L412

ex 1:
args:
  data length = 2 Bytes
  tone freq = 440 Hz
  duration = 500 msec

result:
  sampling_rate = 2 Bytes * 440 Hz = 880 Hz.
  repeat_count = 500 msec * 440 Hz / 1000 = 220 time
ex 2:
args:
  data length = 1000 Bytes
  tone freq = 440 Hz
  duration = 2000 msec

result:
  sampling_rate = 1000 Bytes * 440 Hz = 440000 Hz.
  repeat_count = 2000 msec * 440 Hz / 1000 = 880 time

In other words, whether the length of the data is 2 or 1000, it will be considered as one amplitude and the tone will be played back.

It is important to note that if the waveform data contains multiple amplitudes, it will be played back at a higher pitch than expected, so be sure to specify data for a single amplitude. If you want to specify data for 10 amplitudes, you need to set the playback frequency to one tenth.

patfelst commented 2 years ago

I think you mean "cycle" when you say "amplitude", but yes I understand now, thanks again.

lovyan03 commented 2 years ago

Yes, you are right. I chose the wrong words.

patfelst commented 2 years ago

Hi there again, I hope this is my mistake, but there seems to be a problem with the tone frequency, it alsmost sounds like two channels are playing, as I hear one frequency increasing as expected, but another channel rises, and then goes lower at about 4,000Hz. Please listen to the video.

This is the code in setup() some code after M5.begin(cfg):

  M5.Speaker.begin();
  M5.Speaker.setVolume(64);

  for (int i = 500; i < 8000; i += 500) {
    M5.Speaker.tone(i, 1000, 0, true);
    Serial.printf("Freq = %d Hz\n", i);
    while (M5.Speaker.isPlaying()) { delay(1); }  // Wait for the output to finish.
    delay(100);
  }
  /// stop output sound.
  M5.Speaker.stop();

https://user-images.githubusercontent.com/11342773/155649367-7bae742c-0065-4ad2-8408-43f4f5f8c97d.mov

lovyan03 commented 2 years ago

@patfelst I don't think it's another channel being played. I think it is due to the relationship between the harmonic components of the square wave and the output frequency. The overtones beyond the Nyquist frequency fold back into the audible range and are heard as swells.

By changing the waveform to a sin wave or increasing the output sampling rate, this sound can be suppressed, but it is difficult to eliminate it completely.

Would it be better to change the default tone to sin wave ?

#include <M5Unified.h>

const uint8_t sin_wav[] = { 128, 152, 176, 198, 218, 234, 245, 253, 255, 253, 245, 234, 218, 198, 176, 152, 128, 103, 79, 57, 37, 21, 10, 2, 0, 2, 10, 21, 37, 57, 79, 103 };

void setup(void)
{
  M5.begin();
  M5.Display.setFont(&fonts::DejaVu18);

  { /// By changing the sampling rate, the fold noise changes.
    auto spk_cfg = M5.Speaker.config();
//  spk_cfg.sample_rate =  50000;
    spk_cfg.sample_rate =  96000;
//  spk_cfg.sample_rate = 100000;
//  spk_cfg.sample_rate = 144000;
//  spk_cfg.sample_rate = 192000;
//  spk_cfg.sample_rate = 200000;
    M5.Speaker.config(spk_cfg);
  }
  M5.Speaker.begin();
  M5.Speaker.setVolume(64);
}

void loop(void)
{
  delay(500);
  for (int i = 500; i < 8000; i += 1) {
//  M5.Speaker.tone(i, 1000, 0, true);
    M5.Speaker.tone(i, 1000, 0, true, sin_wav, sizeof(sin_wav));
    M5.Display.setCursor(0, M5.Display.height() / 2);
    M5.Display.printf("Freq = %d Hz        ", i);
    delay(1);
  }

  delay(500);
  for (int i = 500; i < 8000; i += 500) {
//  M5.Speaker.tone(i, 500, 0, true);
    M5.Speaker.tone(i, 500, 0, true, sin_wav, sizeof(sin_wav));
    M5.Display.setCursor(0, M5.Display.height() / 2);
    M5.Display.printf("Freq = %d Hz        ", i);
    while (M5.Speaker.isPlaying()) { delay(1); }
  }
}
patfelst commented 2 years ago

wow @lovyan03 this is really interesting - square wave harmonics, I would not have thought of that!

Is it a processor overhead to use those higher output frequencies (sampling rate)? I'm assuming not. It is much better with the sine wave, and at 100,000 output sampling rate, it is great for my purpose. So I agree, make sine wave the default tone.

I do still hear some harmonics at a few higher frequences (>6kHz) but for my purposes of creating warning tones, it is much better now. Perhaps I should just use proper sampled sound effects. Is there a web site for converting a wav file to c++ array?

Thanks for your help, much appreciated!

patfelst commented 2 years ago

Something strange just happened, I was experimenting with the master volume, and my 10x RGB LEDs driven with Fastled library changed to an unexpected colour (I set purple, it went light blue). I found that if I do not set the master volume, this does not happen (LED is correct colour). Also if I put in a delay as low as 1ms just after setting the LED colour, the problem also does not happen.

If I use M5.Speaker.setAllChannelVolume() the LED problem also does not occur.

If I don't put in a delay, and see the wrong colour, after making a tone, some time later in the main loop, when I set a new colour, the LEDs return to the correct colour again.

Any idea what is going on here? I'd rather not leave an enexplained delay in my code!

Setup() code is:

#define LED_COUNT 10
#define LED_PIN   25

void setup(void) {
  // setup M5Unified LCD, and create a bunch of sprites

  // Setup 10x RGB LED string
  FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, LED_COUNT);
  set_rgb_led(led_brightness_percent, CRGB::Fuchsia); // Fuchsia is purple / pink colour
  delay(1); // Needed to prevent `M5.Speaker.setVolume()` from changing LED colour to light blue. No idea why?

  auto spk_cfg = M5.Speaker.config();
  spk_cfg.sample_rate = 192000; // M5 Unified default = 44,100
  M5.Speaker.config(spk_cfg);

  M5.Speaker.begin();
  // M5.Speaker.setVolume((30 * 255) / 100);  // Set volume to 30% - causes LED to be light blue colour?
  M5.Speaker.setAllChannelVolume((30 * 255) / 100);  // Set volume to 30% - LED is correct colour

  const uint8_t sin_wav[] = {128, 152, 176, 198, 218, 234, 245, 253, 255, 253, 245, 234, 218, 198, 176, 152, 128, 103, 79, 57, 37, 21, 10, 2, 0, 2, 10, 21, 37, 57, 79, 103};
  // const uint16_t C4 = 261.626;
  // const uint16_t E4 = 329.628;
  // const uint16_t G4 = 391.995;
  const uint16_t C6 = 1046.50;
  // const uint16_t E6 = 1318.51;
  const uint16_t G6 = 1567.98;
  uint16_t chord_duration_ms = 200;
  M5.Speaker.tone(C6, chord_duration_ms, 0, false, sin_wav, sizeof(sin_wav));
  M5.Speaker.tone(G6, chord_duration_ms, 1, false, sin_wav, sizeof(sin_wav));

  // other setup stuff
}
lovyan03 commented 2 years ago

@patfelst umm... The code you provided is incomplete, so I cannot reproduce the issue. Please provide us with buildable code.

lovyan03 commented 2 years ago

The develop branch has been updated.

Please give it a try.

patfelst commented 2 years ago

@patfelst umm... The code you provided is incomplete, so I cannot reproduce the issue. Please provide us with buildable code.

sorry about that.

I prepared some buildable code for you with the develop branch, but I can't reproduce the Fastled problem any more. So please ignore that comment above.

Also, the develop branch is great. The default settings sound much better. To be clear, I didn't specify the sin_wave[] array either.

patfelst commented 2 years ago

I should have said thank you once again for your work on this library.

I still have a question about how to generate c++ arrays from audio files. Is there an online tool? Or a command line tool for PC's. Thanks

lovyan03 commented 2 years ago

Perhaps there are others if you look for them.

chuck808 commented 1 year ago

I'm a little late to this thread, but I probably need to be able to produce Tones using the M5Core2 library as I need the Accelerometer also. I have got the Tones working with the M5Unified.

Is there any way to achieve this with M5Core2.h?

lovyan03 commented 1 year ago

Hello, @chuck808 M5Unified's IMU cannot meet your request?

Since M5Core2.h does not have a tone function, it basically has to send sound data to I2S by itself.

chuck808 commented 1 year ago

Hello @lovyan03,

Thank you for the quick reply. I was afraid of that :). It seems a bit odd that M5Core2.h doesn't have a tone function, but I suppose I can use the Speak example to achieve my needs, just a pain to have to create the wav's :(

I am also trying to recreate a 3-axis acceleration monitor (graph display) script from here, but it says it can't compile for Core2 when using the M5Unified.h library.

Regards