Please or Register to create posts and topics.

如何用你的声音玩像飞翔的鸟一样的游戏!

通过使用 AI 为您进行声音分类来赢得时间

项目介绍

欢迎阅读本教程,了解如何使用 Arduino R4 WIFI 板创建类似 Flappy Bird 的游戏。在本指南中,我们将介绍硬件设置以及与代码相关的所有内容,以便您可以重现它。

以下是该项目的主要部分:

  1. 使用 LED 矩阵
  2. 使用麦克风
  3. 自动对要播放的声音进行分类(使用 AI!!
  4. 处理多个循环()
  5. 处理上下移动的球员
  6. 处理一堵有洞的墙向玩家移动并发生碰撞
  7. 跟踪分数
  8. 随着游戏的进行增加难度

重现游戏需要步骤 1、7 和 8。此处的其他步骤将解释代码的每个部分。

第 1 步:设置

首先,我们需要将麦克风连接到Arduino板。

使用跳线连接:

  1. OUT(麦克风)到 A0(板)
  2. GND 到板上的一个 GND
  3. VCC 至 3.3v

确保您有一根 USB 数据线将主板连接到 PC。

在Arduino IDE中:

确保您选择了正确的 COM 端口:工具>端口,然后选择正确的端口。

选择正确的电路板:

  1. Arduino Renesas UNO R4 > 开发板的工具>板 > Arduino UNO R4 WIFI
  2. 如果没有找到它,请单击“Tools > Boards > Boards Manager...”,查找 UNO R4 并安装软件包

选择所需的库,Sketch > Include Library,我们需要包括以下库:

  1. Arduino图形
  2. Arduino_LED_Matrix
  3. 线
  4. 调度

您还可以下载您没有的库:草图>包括库>管理库...

在本教程的后面(步骤 8)中,我们还需要添加 NanoEdge AI 库来自行处理声音,而不是手动处理。

第 2 步:麦克风

要从麦克风获取声音,我们不需要太多:

我们需要定义我们使用的引脚并调用函数 analogRead()

int const AMP_PIN = A0;       // Preamp output pin connected to A0       
void setup() {       
	 ...       
}       
void loop() {       
	 ...       
	 /* Get a sound sample */       
	 static uint16_t sample = 0; //stock values       
	 sample = analogRead(AMP_PIN);       
	 ...       
}       

此示例演示如何从麦克风获取单个值,但在本项目中,我们将使用样本缓冲区。更多内容请见第 7 步。

第 3 步:LED 矩阵

为了玩游戏,我们使用板上的 LED 矩阵。

基本上,我们有一个表示矩阵的数组,我们在其中放置 0(LED 关闭)和 1(LED 打开)。

以下是使用它所需的简单示例:

/* Libraries needed */       
#include "ArduinoGraphics.h"       
#include "Arduino_LED_Matrix.h"       
/* Defines */       
#define HEIGHT            8       
#define WIDTH             12       
/* Object */       
ArduinoLEDMatrix matrix;       
/* Declare matrix to display */       
byte frame[8][12] = {       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },       
	 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }       
};       
void setup() {    
	 ...    
	 /* requiered to use the LED matrix */    
	 matrix.begin();    
	 ...    
}    
void loop() {      
	  ...    
	  /* Change value of the LED matrix */    
	  frame[some x][some y] = 1    
	  /* Update display when needed */    
	   matrix.renderBitmap(frame, HEIGHT, WIDTH);    
	  ...    
}    

根据我们想要显示的内容,有多种方法可以做到这一点。

在这种情况下,我们计算玩家和墙壁的位置,并在 while 循环中更新上面的矩阵。

这是文档: $ https://docs.arduino.cc/tutorials/uno-r4-wifi/led-matrix/ $

我们还使用矩阵在游戏开始时显示文本,以下是我们如何实现它(阅读文档了解更多信息):

void introduction_message()       
{       
	 matrix.beginDraw();       
	 matrix.stroke(0xFFFFFFFF);       
	 matrix.textScrollSpeed(50);       
	 // add the text       
	 const char text[] = " Flappy bird! ";       
	 matrix.textFont(Font_5x7);       
	 matrix.beginText(0, 1, 0xFFFFFF);       
	 matrix.println(text);       
	 matrix.endText(SCROLL_LEFT);       
	 matrix.endDraw();       
}       

第 4 步:循环

为了获得更好的体验,我们使用多个循环:

  1. 一个处理墙的生成、移动、与玩家的碰撞和分数
  2. 第二个处理玩家位置和声音检测以使其移动

两个循环允许我们几乎独立于墙壁移动玩家,我们可以多次移动它,而墙壁只能移动一次。

文档: $ https://www.arduino.cc/reference/en/libraries/scheduler/ $

要使用它,我们只需要导入调度程序库并在设置中创建第二个循环:

/* Libraries needed */       
#include <Scheduler.h>       
void setup() {       
	 ...       
	 /* start a second loop */       
	 Scheduler.startLoop(loop2);       
	 ...       
}       
void loop() {         
	  ...       
	  /* Code related to the wall */       
	  ...       
}       
void loop2() {         
	  ...       
	  /* Code related the player */       
	  ...       
}       

第 5 步:墙壁循环

以下是 Wall 循环中的代码:

void loop() {       
	 if (game_ongoing) {       
	   /* Clean the last column of the matrix */       
	   for (uint8_t y = 0; y < HEIGHT; y++) {       
	     frame[y][WIDTH - 1] = 0;       
	     frame[y][WIDTH - 2] = 0;       
	   }       
	   /* Set wall position on the matrix using random */       
	   wall_start_pix = random(0, 5);       
	   /* Move the wall through matrix */       
	   do {       
	     if (wall_move) {       
	       for (uint8_t y = 0; y < HEIGHT; y++) {       
	         frame[y][wall_pos_x] = (y >= wall_start_pix && y < wall_start_pix + wall_size) ?  0 : 1;       
	         if (wall_pos_x > 1) {       
	           frame[y][wall_pos_x - 2] = 0;       
	         }       
	       }       
	       wall_pos_x++;       
	     }       
	     wall_move = !wall_move;       
	     /* Update display */       
	     matrix.renderBitmap(frame, HEIGHT, WIDTH);       
	     /* Adapt level */       
	     adapt_game_level(); // function in the full code       
	     /* Check if player touch the wall */       
	     if (frame[player_y][player_x] == frame[player_y][player_x - 1]) {       
	       game_ongoing = 0; //stop the game       
	       reset_global_variables(); //function in the full code       
	       return;       
	     }       
	   } while (wall_pos_x < WIDTH);       
	   /* Increment score counter */       
	   score++;       
	   /* Reset wall position */       
	   wall_pos_x = 0;       
	 }       
}       

代码是不言自明的,但这是完成的:

  1. 墙从左向右移动。因为我们正在循环,所以我们需要在到达边缘时删除墙,然后再制作新的墙(可以在循环结束时完成,但我们选择在这里)
  2. 我们随机确定墙上孔的位置。墙的大小为 3,矩阵的高度为 8,因此我们有 5 个可能的起点。
  3. 我们将墙移到屏幕的右端。这意味着将 1 放在新位置,将 0 放在墙的最后一个位置
  4. 更新矩阵
  5. 根据分数增加难度
  6. 检查碰撞并结束游戏 + 根据需要显示分数

第 6 步:播放器循环

这是播放器的循环:

void loop2() {       
	 if (game_ongoing) {       
	   /* make a classification only if we detect a sound */       
	   if (analogRead(AMP_PIN) > 400) {       
	     get_microphone_data(); //function in the full code       
	     neai_classification(neai_buffer, output_class_buffer, &id_class);       
	     /* Player next movement based on class detected */       
	     if (id_class == 2) {       
	       player_move = -1; //up       
	     }       
	     else if (id_class == 3) {       
	       player_move = 1; //down       
	     }       
	   }       
	   else {       
	     //We don't want to repeat the same movement until we detect something else       
	     id_class = 0;       
	     player_move = 0;       
	   }       
	   //move the player       
	   move_player();       
	   /* Clean neai buffer */       
	   memset(neai_buffer, 0.0, AXIS * SENSOR_SAMPLES * sizeof(float));       
	 }       
	 delay(10);       
}       

我们在这里所做的也很简单:

  1. 如果检测到声音(400 是我在什么都没发生时得到的值),我们会做一些事情
  2. 我们从麦克风收集声音
  3. 我们将这种声音分类:要么意味着向上移动,要么意味着向下移动播放器(见下一步)
  4. 根据职业,我们相应地移动玩家

第 7 步:声音分类

为了玩游戏,我们发出两种声音,一种是向上,一种是向下

为了对声音进行分类,我们可以手动完成。我不确定如何,但如果我们计算 FFT 并设置阈值,这应该是可能的。

取而代之的是,我们使用 NanoEdge AI Studio 自动完成,并在我们的代码中包含一个非常小的 AI 模型。

  1. 安装 NanoEdge AI Studio: https://stm32ai.st.com/download-nanoedgeai/ 美元

创建N类分类项目:

  1. 选择麦克风 1 轴作为传感器
  2. 选择Arduino UNO R4 WIFI作为目标

收集数据:

  1. 下面附有 Arduino IDE 的 Flash 代码 (sound_datalogger.ino)
  2. 在 NanoEdge 的 signals 选项卡中,单击 add Signal,然后单击 serial
  3. 首先收集多个声音示例以“向上”播放(大约 100 个示例)
  4. 然后是另一个声音的多个示例来播放“向下”

启动基准测试并获得良好的准确性

编译库

有关如何使用 NanoEdge AI Studio 和 Arduino 的更多详细信息,分步教程: $ https://wiki.st.com/stm32mcu/wiki/AI:How_to_create_Arduino_Rock-Paper-Scissors_game_using_NanoEdge_AI_Studio $

注意:

随附的代码仅包含一个功能,用于收集声音的下采样缓冲区并通过串行发送它们。

我们得到一个下采样缓冲区,因为采集的基本频率非常快,所以我们会得到一个代表几毫秒的缓冲区。通过对它进行缩减采样,它代表了现实生活中的更多时间。

为此,我们只需读取麦克风发送的所有值,但我们只保留 1/8 值。

第 8 步:添加分类

现在我们有了分类库,我们需要将其添加到我们的Arduino代码中:

  1. 打开得到的.zip,有一个Arduino文件夹,里面有另一个zip
  2. 在Arduino IDE中导入库:Sketch > Include library > Add .ZIP library...,然后选择Arduino文件夹中的.zip

然后在我们的代码中,要使用 NanoEdge 分类,我们只需要一些代码,如下例所示:

  1. 我们包括 NanoEdge 库
  2. 我们初始化库
  3. 我们从麦克风收集数据
  4. 我们进行分类

#include "NanoEdgeAI.h"       
#include "knowledge.h"       
/* NanoEdgeAI variables part */       
uint8_t neai_code = 0;       
uint16_t id_class = 0; // Point to id class (see argument of neai_classification fct)       
float output_class_buffer[CLASS_NUMBER]; // Buffer of class probabilities       
const char *id2class[CLASS_NUMBER + 1] = { // Buffer for mapping class id to class name       
	 "unknown",       
	 "up",       
	 "down",       
};       
static float neai_buffer[SENSOR_SAMPLES * AXIS] = {0.0};       
void setup() {       
	 ...       
	 /* initialize NanoEdge Library */       
	 neai_code = neai_classification_init(knowledge);       
	 if (neai_code != NEAI_OK) {       
	   Serial.print("Not supported board.n");       
	 }       
	 ...       
}       
void loop() {         
	  ...       
	  /* make a classification */       
	  get_microphone_data();       
	  neai_classification(neai_buffer, output_class_buffer, &id_class);       
	  /* then depending on the class in id_class, we play up, down or wathever */       
	  ...       
}       

警告:

您需要检查 Arduino 代码中的 id2class 变量是否与我们之前导入的库中的 NanoEdgeAI.h 文件中的变量相同:

在 document/arduino/libraries/nanoedge/src/NanoEdgeAI.h 中(在文件末尾)

const char *id2class[CLASS_NUMBER + 1] = {        
	 "unknown",       
	 "up",       
	 "down",       
};       

第 9 步:最终项目

该项目的完整代码如下 (arduino_demo_flappy_bird_sound.ino)

您需要按照步骤 1 和步骤 7 和 8 使其正常工作!

这个想法是展示如何使用人工智能更快地实现项目。无需手动进行声音分类,而是自动完成,使用 NanoEdge,只需几分钟即可找到模型并对其进行集成。

您可以以此示例为例,并使用相同的方法来争取时间并实现简单的 POC,如果手动完成,可能会花费更多时间。

感谢您阅读:)

代码:声音数据记录仪:

#include <Wire.h>

#define SENSOR_SAMPLES    512

static float neai_buffer[SENSOR_SAMPLES] = {0.0};
static uint16_t neai_ptr = 0;

int const AMP_PIN = A0;       // Preamp output pin connected to A0

/* Prototypes ----------------------------------------------------------*/
void get_microphone_data(void);

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
void loop() {
  // put your main code here, to run repeatedly:
  get_microphone_data();
}


/* Functions declaration ----------------------------------------------------------*/
void get_microphone_data()
{
  static uint16_t temp = 0;
  if (analogRead(AMP_PIN) > 400){
    int sub = 0;
    while(neai_ptr < SENSOR_SAMPLES) {
      if (sub > 8){
        /* Fill neai buffer with new accel data */
        neai_buffer[neai_ptr] = analogRead(AMP_PIN);
        /* Increment neai pointer */
        neai_ptr++;
        sub = 0;
      }
      else{
        temp = analogRead(AMP_PIN);
      }
      sub ++;
    }
    for(uint16_t i = 0; i < SENSOR_SAMPLES; i++) {
       Serial.print(neai_buffer[i]);
       Serial.print(" ");
    }
    Serial.print("n");
    /* Reset pointer */
    neai_ptr = 0;
  }
}

主代码

/* Libraries ----------------------------------------------------------*/
#include "ArduinoGraphics.h"
#include "Arduino_LED_Matrix.h"
#include <Wire.h>
#include "NanoEdgeAI.h"
#include "knowledge.h"
#include <Scheduler.h>
/* Defines  ----------------------------------------------------------*/
/* Matrix part */
#define HEIGHT            8
#define WIDTH             12
#define PLAYER_X          10  //initial player position
#define PLAYER_Y          4
/* NEAI part */
#define SENSOR_SAMPLES	  512
#define AXIS              1

/* Prototypes ----------------------------------------------------------*/
void introduction_message(void);
void get_microphone_data(void);
void adapt_game_level(void);
void print_score(uint16_t game_score);
void reset_global_variables(void);

/* Objects  ----------------------------------------------------------*/
ArduinoLEDMatrix matrix;

/* Global variables ----------------------------------------------------------*/
int game_ongoing = 0;
static byte wall_move = false;
static int8_t player_move = 0;
static uint8_t player_x = PLAYER_X, player_y = PLAYER_Y;
static uint8_t wall_start_pix = 0, wall_pos_x = 0, wall_size = 3;
static uint16_t score = 0, neai_ptr = 0, time_to_wait = 50;
static float neai_buffer[SENSOR_SAMPLES * AXIS] = {0.0};

int const AMP_PIN = A0;       // Preamp output pin connected to A0

/* NanoEdgeAI variables part */
uint8_t neai_code = 0;
uint16_t id_class = 0; // Point to id class (see argument of neai_classification fct)
float output_class_buffer[CLASS_NUMBER]; // Buffer of class probabilities
const char *id2class[CLASS_NUMBER + 1] = { // Buffer for mapping class id to class name
  "unknown",
  "up",
  "down",
};

/* Declare matrix to display */
byte frame[8][12] = {
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
};

/* Setup function ----------------------------------------------------------*/
void setup() {
  Serial.begin(115200);
  Scheduler.startLoop(loop2); //first loop is wall movement, second is player

  matrix.begin();
  introduction_message(); //display a message when starting

  /* Initialize NanoEdgeAI AI */
  neai_code = neai_classification_init(knowledge);
  if (neai_code != NEAI_OK) {
    Serial.print("Not supported board.n");
  }
  else {
    game_ongoing = 1; //launch game if nanoedge working correctly
  }
}

/* Infinite loop ----------------------------------------------------------*/
void loop() {
  if (game_ongoing) {
    /* Clean the last column of the matrix */
    for (uint8_t y = 0; y < HEIGHT; y++) {
      frame[y][WIDTH - 1] = 0;
      frame[y][WIDTH - 2] = 0;
    }

    /* Set wall position on the matrix using random */
    wall_start_pix = random(0, 5);

    /* Move the wall through matrix */
    do {
      if (wall_move) {
        for (uint8_t y = 0; y < HEIGHT; y++) {
          frame[y][wall_pos_x] = (y >= wall_start_pix && y < wall_start_pix + wall_size) ?  0 : 1;
          if (wall_pos_x > 1) {
            frame[y][wall_pos_x - 2] = 0;
          }
        }
        wall_pos_x++;
      }
      wall_move = !wall_move;

      /* Update display */
      matrix.renderBitmap(frame, HEIGHT, WIDTH);

      /* Adapt level */
      adapt_game_level();

      /* Check if player touch the wall */
      if (frame[player_y][player_x] == frame[player_y][player_x - 1]) {
        game_ongoing = 0; //stop the game
        reset_global_variables();
        return;
      }
    } while (wall_pos_x < WIDTH);
    /* Increment score counter */
    score++;
    /* Reset wall position */
    wall_pos_x = 0;
  }
}


void loop2() {
  if (game_ongoing) {
    /* make a classification only if we detect a sound */
    if (analogRead(AMP_PIN) > 400) {
      get_microphone_data();
      neai_classification(neai_buffer, output_class_buffer, &id_class);
      /* Player next movement based on class detected */
      if (id_class == 1) {
        player_move = -1; //up
      }
      else if (id_class == 2) {
        player_move = 1; //down
      }
    }
    else {
      //We don't want to repeat the same movement until we detect something else
      id_class = 0;
      player_move = 0;
    }
    //move the player
    move_player();

    /* Clean neai buffer */
    memset(neai_buffer, 0.0, AXIS * SENSOR_SAMPLES * sizeof(float));
  }
  delay(1);
}

/* Functions declaration ----------------------------------------------------------*/
void introduction_message()
{
  matrix.beginDraw();
  matrix.stroke(0xFFFFFFFF);
  matrix.textScrollSpeed(50);

  // add the text
  const char text[] = " Flappy bird! ";
  matrix.textFont(Font_5x7);
  matrix.beginText(0, 1, 0xFFFFFF);
  matrix.println(text);
  matrix.endText(SCROLL_LEFT);

  matrix.endDraw();
}

void move_player() {
  /* Move the player and check if it stays in the screen */
  if (player_y + player_move >= 0 && player_y + player_move < HEIGHT) {
    /* Clear player last position */
    frame[player_y][player_x] = 0;
    /* Change player y coordinate */
    player_y += player_move;
  }
  /* Display player in matrix */
  frame[player_y][player_x] = 1;

  /* Update display */
  matrix.renderBitmap(frame, HEIGHT, WIDTH);
}

/* function to get a single downsampled sample from sensor 
We get every values but keep only one every eight values because the model was train with data like this.
It permits to listen for a longer period of time and get a better accuracy
*/
void get_microphone_data()
{
  static uint16_t temp = 0; //stock values
  int sub = 0; //increment to downsample
  //while the buffer is not full
  while (neai_ptr < SENSOR_SAMPLES) {
    //if it is the eighth value
    if (sub > 8) {
      /* Fill neai buffer with new accel data */
      neai_buffer[neai_ptr] = analogRead(AMP_PIN);
      /* Increment neai pointer */
      neai_ptr++;
      sub = 0; //reset increment
    }
    else {
      temp = analogRead(AMP_PIN);
    }
    sub ++;
  }
      for(uint16_t i = 0; i < SENSOR_SAMPLES; i++) {
       Serial.print(neai_buffer[i]);
       Serial.print(" ");
    }
    Serial.print("n");
  neai_ptr = 0;
}

void adapt_game_level()
{
  /* Adapt speed & hole size */
  if (score < 5) {
    time_to_wait = 50;
  }
  else if (score >= 5 && score < 10) {
    time_to_wait = 40;
  }
  else if (score >= 10 && score < 15) {
    time_to_wait = 30;
  }
  else if (score >= 15 && score < 20) {
    wall_size = 2;
  }
  else {
    wall_size = 1;
  }
  delay(time_to_wait);
}

void print_score(uint16_t game_score)
{
  uint8_t text_pos_x = (game_score < 10) ? 5 : 3;
  matrix.clear();
  matrix.beginDraw();
  matrix.stroke(0xFFFFFFFF);
  char text[3];
  itoa(game_score, text, 10);
  matrix.textFont(Font_4x6);
  matrix.beginText(text_pos_x, 1, 0xFFFFFF);
  matrix.println(text);
  matrix.endText();
  matrix.endDraw();
}

void reset_global_variables()
{
  memset(frame, 0, WIDTH * HEIGHT * sizeof(byte));
  player_x = PLAYER_X;
  player_y = PLAYER_Y;
  print_score(score);
  /* Reset score after loosing the party */
  score = 0;
  /* Reset wall position */
  wall_pos_x = 0;
  /* Reset wall size */
  wall_size = 3;
  /* Reset delay */
  time_to_wait = 50;
  delay(1000);
  game_ongoing = 1;
}