Saturday, November 16, 2024
Google search engine
HomeUncategorisedProject Idea | Build an AR based music learning platform using the...

Project Idea | Build an AR based music learning platform using the MERN Stack

In this article, we will be building an AR-based web app called GuitAR. It teaches users to learn guitar by projecting the strings that are to be plucked on the guitar in the user’s camera feed. Now, all the user has to do is to pluck the highlighted strings to play a particular song. Here is an sample image for better understanding.

 Final Result. PS: We are using phone here but a printed marker will give better result.

We will be building this project with React JS for the front-end, ExpressJS running on NodeJS for back-end and MongoDB for database. We will also be using Selenium, BeautifulSoup4, Pymango for building our database of songs and ArucoJS which is an popular CV library for JavaScript. We will also use Firebase Authentication to integrate Google login into the app. Finally, we will use Github Actions to deploy the project on heroku!

This article assumes an understanding of MERN Stack, web scrapping, and some trigonometry and coordinate geometry. We won’t be going into details of the basic frontend and backend code because there are already many great articles covering it, and therefore we will focus on mostly on AR part of the project. You can check out the complete code in Github Repos. 

Links:

Note: Before getting started with the technical stuff let’s get acquainted with some guitar jargon. 

  • Guitar tablature usually referred to as “tab”, is a method of notating music that empowers beginner guitarists to learn songs quickly and easily. Guitar tabs share similarities with music staff notation by showing you what notes to play, how long to play them, and what techniques to use. These tabs will keep changing throughout a song. We will be required to maintain where these tabs change in relation to lyrics in the database. 
  • The fingerboard (also known as a fretboard on fretted instruments) is an important component of most stringed instruments. It is a thin, long strip of material, usually wood, that is laminated to the front of the neck of an instrument. The strings run over the fingerboard, between the nut and bridge. To play the instrument, a musician presses strings down to the fingerboard to change the vibrating length, changing the pitch. This is called stopping the strings.

 

Step 1: Building the database — First, we will be creating a database on MongoDB atlas. This database will contain the lyrics and the tabs mentioned earlier along with user settings. Login to MongoDB, create a cluster and create a database. We will be scrapping https://www.ultimate-guitar.com/ but feel free to any other method of your choice. The script for the spider can be found in the links section. Once you are done scrapping, save the data into database using the below code:

Python3




import os
import pymongo
from dotenv import load_dotenv
load_dotenv()
  
  
class Mongo:
    MONGODB_PASSWORD = os.environ.get("MONGODB_PASSWORD")
  
    # Replace this with your connection url from mongodb
    connection_url = "mongodb+srv://admin:" 
        + MONGODB_PASSWORD + 
        "@cluster0.jx3fl.mongodb.net/guitarappdb?retryWrites=true&w=majority"
    print(connection_url)
  
    client = pymongo.MongoClient(connection_url)
  
    songs = db["songsv2"]
  
    def __getSchema(self, title, artist, info1, info2, info3, data):
        return {
            "title": title,
            "artist": artist,
            "info1": info1,
            "info2": info2,
            "info3": info3,
            "data": data
        }
  
    def addSong(self, title, artist, info1, info2, info3, data):
        id = self.songs.insert_one(self.__getSchema(
            title, artist, info1, info2, info3, data
        ))


At the end of it the database should look something like this where <tag></tag> encloses the tabs:

_id:ObjectId("604c6b18ab0d440efde7ae4f")
title:"Drivers License"
artist:"Olivia Rodrigo"
info1:""
info2:"Difficulty: novice"
info3:""
data:"[Verse 1]
      <tag>G</tag>  I got my driver’s license last week
       Just lik..."

Step 2: Build backend

1. Setup — Create a folder and run npm init in it. After completing setup edit package.json to make it look like this:

// package.json

{
  "name": "guitar-app-backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "<YOUR_NAME>",
  "license": "ISC",
}

This will allow using ES6 syntax in our app. Now install express, mongoose, dotenv, cors and nodemon using npm or yarn.

2. Create Schema — We will first create the schema to access the songs that we have added to the Db earlier.

Javascript




// dbSongs.js
import mongoose from 'mongoose'
  
const songsSchema = mongoose.Schema({
    title : String,
    artist : String,
    info1 : String,
    info2 : String,
    info3 : String,
    data : String
})
  
export default mongoose.model('songsv2', songsSchema)


Now, we can create the user schema to save user settings. Since we are not storing any sensitive information like passwords or financial data, we can make do without any sort of security like encryption. We will discuss fields like “textColors” and “guitar” while building the frontend.

Javascript




// dbUser.js
import mongoose from 'mongoose'
  
const userSchema = mongoose.Schema({
    email : String,
    displayName : {type : String, default : "User"},
    photoUrl : 
        {type : String, default : "<DEFAULT_PROFILE_PICTURE_URL>"},
    speed : {type : Number, default : 1},
    strokeColor : {type : String, default : "red"},
    textColor : {type : String, default : "black"},
    guitar : {
        m_ratio1 : {type : Number, default : 0},
        n_ratio1 : {type : Number, default : 0},
        m_ratio2 : {type : Number, default : 0},
        n_ratio2 : {type : Number, default : 0},
        m_ratio3 : {type : Number, default : 0},
        n_ratio3 : {type : Number, default : 0},
        m_ratio4 : {type : Number, default : 0},
        n_ratio4 : {type : Number, default : 0},
    }
})
  
export default mongoose.model('userInfo', userSchema)


3. Create Endpoints — Now we will write the endpoints for the APIs. The complete code for the endpoints can be found in the backend repo in the links section. Once the backend is complete, it is ready to test. Run nodemon server.js and use postman for testing the API. Make sure to setup the .env file or change the mongodb connection URL to your own URL (not recommended if you plan to make your code public) before running.

4. Setup CI/CD — Push the code to GitHub in your repository using git. Create your Heroku app and add Heroku API key and mongodb password in GitHub secrets. Also, add the MongoDB password in Heroku environment. Now, go to your repository, go to actions and set up a new workflow as follows:

name: Build And Deploy

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:
    
    name : Build
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x, 15.x]
    env:
      MONGODB_PASSWORD: ${{secrets.MONGODB_PASSWORD}}

    steps:
    - uses: actions/checkout@v2
    - name: Install Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        
    - name: Install NPM Package
      run: npm ci
      
    - name: Build project
      run: npm run build --if-present
      env:
        CI: false
  
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      - name: Push to Heroku
        uses: akhileshns/heroku-deploy@v3.4.6
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "<YOUR_HEROKU_APP_NAME_HERE>"
          heroku_email: "<YOUR_HEROKU_EMAILID_HERE>"

      - name: Run a one-line script
        run: echo successfully run

Note: If you plan to add CI/CD, make sure you use .env file for mongodb connection URL.

Step 3: Setup Frontend

1.  Create react app — Use npx create-react-app to create react app. Clean the default app and install the following dependencies using npm or yarn:

Dependency Function
Aruco JS Computer Vision
React GSAP Animation
Axios Networking
Firebase Google Authentication
Material UI Core UI
Material UI Icons UI
React Redux Global Props
React Router Routing
React Player Embed Vimeo Video
React Webcam Access webcam
Three JS 3D Graphics
Vanta JS Animated backgrounds

2. Build the Login page — This page will use Firebase to show the Google SignIn popup to the user. After getting Google account data, consume /api/v1/users/findbyemail and /api/v1/users/create to create new user or get user settings, if the user already exists. Once you have the user data from the database, store it into redux so that it can be accessed from anywhere. You get complete code from the frontend repo in the links section. This is what the result should look like:

Login Page

 

3. Build the Songs page — This page will contain a list of various songs that the user can play. This can be built by consuming /api/v1/songs/get. This endpoint takes page, limit, and query as request query which will help in pagination and search functionality. It should also contain a navbar to navigate to different pages. You get complete code from frontend repo in links section. This is how the page should look like:

All Songs Page

 

4. Add Settings Page — This page will let the user set the color of the AR projections and the speed of songs stored in user schema of the database. The current values can be accessed from the user data we stored in redux. We can update these settings by using /api/v1/users/updateStrokeColor, /api/v1/users/updateSpeed and, /api/v1/users/updateTextColor. It should look like this after completion:

Settings Page

 

 

Step 4: Building AR Helper Functions

Now, we are down to the toughest but most interesting part of the project. Before getting started, lets understand our approach to the AR problem. Before going for the solution, let’s consider some assumptions that we have made about the problem for an easier solution.

Assumptions:

  • The position of marker on the guitar does not change after the setup procedure. This allows us to store the positional data during setup and use it for plotting later on. If position is changed, user has to go through setup again.
  • The guitar is parallel to the camera. This reduces the problem into a 2D one which is much easier to solve and also reduces the computation. Computing the 3rd dimension doesn’t add much functionality when compared to the extra setup and computation required to resolve it. We can check this condition by making sure that all edges of marker are of same length as the marker is square.
  • We trust users to select the four corners of the fretboard precisely during the setup process.

Now to locate the fretboard, we consider three corners of marker as a coordinate system such that the central corner lies on the origin and the rest two on the x and y-axis. Now that we have our axes, we have to worry about mapping points on these axes. Instead of storing the actual lengths of the x and y coordinates, we store their ratio with the size of marker which allows us to scale the distances depending upon how far the marker is from the camera.

Mapping points wrt marker

Now that we have mapped the points wrt the marker, we need to convert them wrt to canvas coordinates. We can easily do this by some basic trigonometry.

x = x_{origin} + x_{len}\times m \times cos(\Theta ) + y_{len}\times n \times sin(\Theta )

y = y_{origin} + x_{len}\times m \times sin(\Theta ) + y_{len}\times n \times cos(\Theta )

Now we can code the same in JavaScript:

Javascript




export default function Map  (origin_x, origin_y , x_axis_x, 
                               x_axis_y, m_ratio, n_ratio) {
      
    // Offset prevent division by 0
    const theta =  Math.atan((x_axis_y - origin_y) / 
        (x_axis_x - origin_x + 0.000000001))
      
    const m = Math.sqrt(Math.pow((origin_x - x_axis_x), 2)
        + Math.pow((origin_y - x_axis_y), 2)) * m_ratio
          
    const n = Math.sqrt(Math.pow((origin_x - x_axis_x), 2) 
        + Math.pow((origin_y - x_axis_y), 2)) * n_ratio
      
    var x_val = origin_x + m * Math.cos(theta) + n * Math.sin(theta)
      
    if(origin_x - x_axis_x > 0){
        x_val = origin_x - m * Math.cos(theta) - n * Math.sin(theta)
    }
      
    var y_val = origin_y + m * Math.sin(theta) - n * Math.cos(theta)
      
    if(origin_x - x_axis_x > 0){
        y_val = origin_y - m * Math.sin(theta) + n * Math.cos(theta)
    }
  
    return {
        x : x_val,
        y : y_val
    }
}


Now that we have the four corners that we need to get the position of the strings on the board. We can easily do that by splitting the rectangle into equal parts. This can be done by the following functions:

 

Javascript




const SplitLine =  (pt1, pt2, n) => {
    var ans = []
    n--;
    for(var i = 0; i <= n; i ++){
        var x = (pt1.x * (n - i)  + pt2.x * i) / n
        var y = (pt1.y * (n - i)  + pt2.y * i) / n
        ans.push({
            x : x,
            y : y
        })
    }
    return ans
}
  
export default SplitLine


Now that we have these points, all we have to do is to join opposite points to draw string. But before we start drawing, we need to find intersection points of two strings to highlight the strings to be pressed. We can do that using the following function using basic geometry: 

Javascript




const FindIntersection = (A, B, C, D) => {
    var a1 = B.y - A.y;
    var b1 = A.x - B.x;
    var c1 = a1*(A.x) + b1*(A.y);
         
    var a2 = D.y - C.y;
    var b2 = C.x - D.x;
    var c2 = a2*(C.x)+ b2*(C.y);
         
    var determinant = a1*b2 - a2*b1;
         
    if (determinant === 0)
    {
        return {x: -1, y: -1}
    }
    else
    {
        var x = (b2 * c1 - b1 * c2) / determinant;
        var y = (a1 * c2 - a2 * c1) / determinant;
        return {x: x, y: y};
    }
}
  
export default FindIntersection


Now we have everything that we need to draw on our canvas. This can be done using the below code which takes canvas to be drawn on, video feed, the current position of marker, and list of points to be highlighted along with some settings as its parameter:

Javascript




import FindIntersection from "../Utils/FindIntersection";
import SplitLine from "../Utils/SplitLine";
import Map from "../Utils/Map";
const AR = require("js-aruco").AR;
  
const draw = (canvas, video, ptr, list, textColor, strokeColor) => {
      
  // list is the list of points to be highlighted
  var ctx = canvas.getContext("2d", { alpha: false });
  
  canvas.width = video.video.videoWidth;
  
  canvas.height = video.video.videoHeight;
  
  ctx.translate(canvas.width, 0);
  ctx.scale(-1, 1);
  ctx.drawImage(video.video, 0, 0, canvas.width, canvas.height);
  ctx.scale(-1, 1);
  ctx.translate(-canvas.width, 0);
  ctx.lineWidth = 5;
  const detector = new AR.Detector();
  var markers = detector.detect(ctx.getImageData(0, 0, 1280, 720));
  
  if (markers.length > 0) {
    const corners = markers[0].corners;
    let pt1, pt2, pt3, pt4;
    pt1 = Map(
      corners[1].x,
      corners[1].y,
      corners[0].x,
      corners[0].y,
      ptr.m_ratio1,
      ptr.n_ratio1
    );
    pt2 = Map(
      corners[1].x,
      corners[1].y,
      corners[0].x,
      corners[0].y,
      ptr.m_ratio2,
      ptr.n_ratio2
    );
    pt3 = Map(
      corners[1].x,
      corners[1].y,
      corners[0].x,
      corners[0].y,
      ptr.m_ratio3,
      ptr.n_ratio3
    );
    pt4 = Map(
      corners[1].x,
      corners[1].y,
      corners[0].x,
      corners[0].y,
      ptr.m_ratio4,
      ptr.n_ratio4
    );
  
    ctx.strokeStyle = `#${strokeColor}`;
  
    ctx.beginPath();
    ctx.moveTo(pt1.x, pt1.y);
    ctx.lineTo(pt2.x, pt2.y);
    ctx.lineTo(pt3.x, pt3.y);
    ctx.lineTo(pt4.x, pt4.y);
    ctx.lineTo(pt1.x, pt1.y);
    ctx.stroke();
  
    var top = SplitLine(pt1, pt2, 6);
    var bottom = SplitLine(pt4, pt3, 6);
  
    var i;
  
    for (i = 0; i < top.length; i++) {
      ctx.beginPath();
      ctx.moveTo(top[i].x, top[i].y);
      ctx.lineTo(bottom[i].x, bottom[i].y);
      ctx.stroke();
    }
  
    var right = SplitLine(pt3, pt2, 6);
    var left = SplitLine(pt4, pt1, 6);
    right.reverse();
    left.reverse();
  
    for (i = 0; i < right.length; i++) {
      ctx.beginPath();
      ctx.moveTo(right[i].x, right[i].y);
      ctx.lineTo(left[i].x, left[i].y);
      ctx.stroke();
    }
  
    if (list) {
      for (var pos = 0; pos < list.length; pos++) {
        ctx.font = "30px Arial";
        ctx.fillStyle = `#${textColor}`;
        var res = FindIntersection(
          top[list[pos].x - 1],
          bottom[list[pos].x - 1],
          {
            x: (left[list[pos].y - 1].x + left[list[pos].y].x) / 2,
            y: (left[list[pos].y - 1].y + left[list[pos].y].y) / 2,
          },
          {
            x: (right[list[pos].y - 1].x + right[list[pos].y].x) / 2,
            y: (right[list[pos].y - 1].y + right[list[pos].y].y) / 2,
          }
        );
        ctx.fillText(`${list[pos].fing}`, res.x, res.y + 5);
      }
    } else {
      console.error("Tab Not Found");
    }
  }
  
  ctx.stroke();
};
  
export default draw;


Now we are already with the code required to the AR part of the project, all that is left is to get the m_ration and the n_ration from user click during the setup procedure. This can be done by reversing the mapping function.

Javascript




const Plot = (origin_x, origin_y , x_axis_x, 
                 x_axis_y, x_val, y_val) => {
  
    const theta =  Math.atan((x_axis_y - origin_y) 
            / (x_axis_x - origin_x + 0.000000001))
  
    let a , b;
    if(origin_x - x_axis_x > 0){
        a = origin_x - x_val
        b = origin_y - y_val        
    } else {
        a = x_val - origin_x
        b = y_val - origin_y
    }
  
    const sin = Math.sin(theta)
    const cos = Math.cos(theta)
  
    const m = a * cos - b * sin
    const n = a * sin - b * cos
  
    const len =Math.sqrt(
        (Math.pow((origin_x - x_axis_x), 2) + 
        Math.pow((origin_y - x_axis_y), 2)))
  
    return {
        m_ratio : m / len,
        n_ratio : n / len
    }
}
  
export default Plot


Step 5: Build The AR Components — Finally, we are ready with all the helper functions required for building the AR components. Now we can integrate them with the views by calling the draw function.

1. Building Setup Page — This page allows user to select the four corners of his guitar fretboard. We will convert this coordinates of each corner into m_ration and n_ration using the plot function that we built earlier. Once we have the rations for all four corners, we can consume /api/v1/users/updateGuitar to save these updates to our database. Once you are done, the page should look like this:

 

2. Storing Song — Before making the practice page, we need a way to process the songs that we get from the backend and store them. This can be done by the following functions:

Javascript




const Song = (s) => {
    const LYRICS = 'LYRICS'
    const TAB = 'TAB'
      
    const list = []
  
    const getNext = (pos) => {
        pos += 5;
        var tab = ""
        var n = s.length;
        while(pos < n){
            if(s.charAt(pos) === '<')
                break;
            tab += s.charAt(pos)
            pos++;
        }
        return {
            pos : pos + 5,
            tab : tab
        }
    }
  
    var text = ""
    for (var i = 0; i < s.length; i++) {
        var c = s.charAt(i);
        if(c === '<'){
            if(text !=="")
                list.push({
                    data: text,
                    type: LYRICS
                })
            var tab;
            var res = getNext(i);
            tab = res.tab
            i = res.pos
            text = ""
            list.push({
                type: TAB,
                data: tab
            })
        } else
            text += s.charAt(i)
    }
  
    if(text !== '')
        list.push({
            type:LYRICS,
            data:text
        })
    return list
  
}
  
export default Song


3. Building Songs Page — Now we are finally ready to build the page where user will actually practice guitar. Firstly on loading, we will load the song from the query. We will also have state called pos, tab and text which will be our position in the song list, current tab and the lyrics to be displayed respectively. After every speed interval (Eg, after 2 sec if speed is 2) we will update these variables to the next ones in the song. Finally, we will be calling the draw function inside requestAnimationFrame to update the canvas efficiently. The complete code can be found below:

Javascript




import React, { useState, useRef, useEffect } from "react";
import Webcam from "react-webcam";
import axios from "../../Utils/axios";
import "./PlaySong.css";
import { useSelector } from "react-redux";
import Song from "./../../Utils/Song";
import tabs from "./../../Utils/Tabs";
import Promt from "./../../components/Promt/Promt";
import Image from "./../../images/finger_coding.jpg";
import Draw from "../../hooks/Draw";
import RotateDevice from 
    "./../../animations/RotateDevice/RotateDevice";
  
export default function PlaySong(props) {
  const User = useSelector((state) => state.isLoggedIn).user;
  
  const webcamRef = useRef(null);
  const canvasRef = useRef(null);
  const points = useRef(null);
  const song = useRef(null);
  const pos = useRef(0);
  const tab = useRef(null);
  const getNextTimerId = useRef(null);
  const playButton = useRef(null);
  const strokeColor = useRef("Red");
  const textColor = useRef("Red");
  const speed = useRef(2);
  
  const [Text, setText] = useState("");
  const [IsPaused, setIsPaused] = useState(true);
  const [IsLanscape, setIsLanscape] = useState(
    window.matchMedia("(orientation: landscape)").matches
  );
  
  var supportsOrientationChange = "onorientationchange" in window,
    orientationEvent = supportsOrientationChange
      ? "orientationchange"
      : "resize";
  window.addEventListener(
    orientationEvent,
    function () {
      setIsLanscape(window.matchMedia(
          "(orientation: landscape)").matches);
    },
    false
  );
  
  function getNext() {
    console.log(pos.current);
    const LYRICS = "LYRICS";
    if (song.current) {
      if (pos.current < song.current.length) {
        if (song.current[pos.current].type === LYRICS) {
          setText(song.current[pos.current].data);
          pos.current = pos.current + 1;
        }
      }
      if (pos.current < song.current.length) {
        tab.current = song.current[pos.current].data;
        pos.current = pos.current + 1;
      }
      if (pos.current >= song.current.length) {
        if (playButton.current) 
            playButton.current.classList.toggle("pause");
        setIsPaused(true);
        pos.current = 0;
        setText("");
        tab.current = null;
        return;
      }
    }
  
    getNextTimerId.current = 
        setTimeout(getNext, speed.current * 1000);
  }
  
  const getAnimation = () => {
    const video = webcamRef.current;
    const canvas = canvasRef.current;
    const ptr = points.current;
    if (video && canvas) {
      var list = tabs.get(tab.current);
      Draw(canvas, video, ptr, list, 
          textColor.current, strokeColor.current);
      window.requestAnimationFrame(() => {
        return getAnimation();
      });
    }
  };
  
  useEffect(() => {
    var Query = new URLSearchParams(props.location.search);
    const getSongUrl = "/api/v1/songs/getFromTitle?title=" 
                + Query.get("title");
    axios.get(getSongUrl).then((res, err) => {
      if (err) alert(err);
      else {
        song.current = Song(res.data.data);
        console.log(song.current);
      }
    });
  
    const getAccountUrl = 
        "/api/v1/users/findbyemail?email=" + User.email;
    axios.get(getAccountUrl).then((res, err) => {
      if (err) alert(err);
      else {
        points.current = res.data.guitar;
        speed.current = res.data.speed;
        textColor.current = res.data.textColor
        strokeColor.current = res.data.strokeColor
      }
    });
  
    window.requestAnimationFrame(getAnimation);
  }, []);
  
  const videoConstraints = {
    width: 1280,
    height: 720,
    facingMode: "user",
  };
  
  const playPause = (e) => {
    e.target.classList.toggle("pause");
    if (IsPaused) getNext();
    else if (getNextTimerId.current) 
        clearTimeout(getNextTimerId.current);
  
    setIsPaused(!IsPaused);
  };
  
  return (
    <>
      {!IsLanscape ? (
        <RotateDevice />
      ) : (
        <div className="playsong__container">
          <canvas ref={canvasRef} className="playsong__canvas" />
          <div className="play__pause__button__container">
            <div
              class="play play__pause__button"
              onClick={playPause}
              ref={playButton}
            />
          </div>
          {Text === "" ? null : <p className="lyrics">{Text}</p>
}
          <div className="playsong__promt_area">
            <Promt
              text="Rock On!"
              description="Press fret with same number as below "
              img={Image}
            />
          </div>
          <Webcam
            className="playsong__cam"
            audio={false}
            ref={webcamRef}
            style={{ width: "0%", height: "0%" }}
            videoConstraints={videoConstraints}
          />
        </div>
      )}
    </>
  );
}


In the end, this page should look like this:

 

4. Setup CI/CD — Push the code to Github in your repository using git. Create your Heroku app and add Heroku API key in GitHub secrets. Now, go to your repository, go to actions and set up a new workflow as follows:

name: Build And Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    
    name : Build
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x, 15.x]
        # See supported Node.js release schedule 
        # at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v2
    - name: Install Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
        
    - name: Install NPM Package
      run: npm ci
      
    - name: Build project
      run: npm run build
      env:
        CI: false
  
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      - name: Push to Heroku
        uses: akhileshns/heroku-deploy@v3.4.6
        with:
          heroku_api_key: ${{secrets.HEROKU_API_KEY}}
          heroku_app_name: "<HEROKU_APP_NAME_HERE>"
          heroku_email: "<YOUR_HEROKU_EMAIL_HERE>"

      - name: Run a one-line script
        run: echo successfully run

Output: Now we can try out the deployed app and it should look like this:

Applications:

  • Guit.ar improves learning speed by around 40%. It helps to build muscle memory by giving live visual feedback to the user. Visual feedback (VFb) is proven to boost the acquisition and retention stages of motor learning associated with training in a reaching task like learning guitar.
  • Guit.ar is a great complement for music institutions and universities. This is helpful for distant learning, especially during the lockdown.
  • Learning Guitar comes with learning a lot of redundant stuff before you play any song. Guit.ar helps to cut the clutter and get started with playing interesting music right from the first day.
  • Users can turn their guitar into a musical game using Guit.ar. It turns a guitar into an arcade game where the objective is to strike the highlighted string within a particular time.

Teammate: https://auth.geeksforgeeks.org/user/ayushpandya517/

Whether you’re preparing for your first job interview or aiming to upskill in this ever-evolving tech landscape, neveropen Courses are your key to success. We provide top-quality content at affordable prices, all geared towards accelerating your growth in a time-bound manner. Join the millions we’ve already empowered, and we’re here to do the same for you. Don’t miss out – check it out now!

RELATED ARTICLES

Most Popular

Recent Comments