What is the best way to create a function that shifts a musical note up or down by one semitone?

Currently developing a guitar tuning tool and facing some hurdles.

Striving to create a function that can take a musical note, an octave, and a direction (up or down), then produce a transposed note by a half step based on the traditional piano layout (i.e., transitioning from B to C will involve adjusting octaves).

The functionality should also handle sharps.

Some examples:

const tunedNoteUp = pitchShiftNoteByHalfStep({note: "B", octave: 3}, "up")
console.log(tunedNoteUp) // {note: "C", octave: 4}

const secondCase = pitchShiftNoteByHalfStep({note:"G#", octave: 4}, "up")
console.log(secondCase) // {note: "A", octave: 4}

const thirdCase = pitchShiftNoteByHalfStep({note: "A#", octave: 4}, "down")
console.log(thirdCase) // {note: "A", octave: 4}

Open to utilizing libraries but haven't found any suitable ones yet.

Current progress:

function pitchShiftNoteByHalfStep(note, octave, direction) {
  const notes = [ 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];

  const index = notes.indexOf(note);
  const newIndex = direction === 'up' ? index + 1 : index - 1;
  const newNote = notes[newIndex >= 0 ? newIndex : 11];

  const newOctave = (octave + Math.floor((newIndex + (direction === 'up' ? 0 : 11)) / 12));

  return `${newNote}${newOctave}`
}

Encountering issues in certain scenarios like shifting G# upwards.

Thank you!

Answer №1

Consider the following:

  • To make note management easier, start the array of note names with "C" as octaves start at C, not A.
  • Instead of using "up" and "down", pass a signed integer (1 for up, -1 for down) to allow for adding more than one half note at a time.
  • Utilize the remainder operator (%) for calculation convenience.

const NOTES = "C,C#,D,D#,E,F,F#,G,G#,A,A#,B".split(",");

function pitchShift({note, octave}, add) {
    const offset = NOTES.indexOf(note) + octave * 12 + add,
          noteNum = offset % 12;
    if (offset < 0 || offset >= 8*12) return null; // Indication of out-of-range.
    return {note: NOTES[noteNum], octave: (offset - noteNum) / 12};
}

// demo
let pitch = {note: "C", octave: 4};
console.log(JSON.stringify(pitch));
// Add 4 half notes to arrive at E:
pitch = pitchShift(pitch, 4);
console.log(JSON.stringify(pitch));
// Subtract 6 half notes to arrive at A# of previous octave
pitch = pitchShift(pitch, -6);
console.log(JSON.stringify(pitch));
// Add 3 half notes to arrive at C#:
pitch = pitchShift(pitch, 3);
console.log(JSON.stringify(pitch));
// Subtract 2 half notes to arrive at B:
pitch = pitchShift(pitch, -2);
console.log(JSON.stringify(pitch));
// Subtract to go to lowest pitch that is supported (C octave 0)
pitch = pitchShift(pitch, -47);
console.log(JSON.stringify(pitch));
// Subtract one more to get an out of range indication (null)
pitch = pitchShift(pitch, -1);
console.log(JSON.stringify(pitch));
// Define second-highest supported pitch (A# octave 7) and add 1 (B octave 7)
pitch = pitchShift({note: "A#", octave: 7}, 1);
console.log(JSON.stringify(pitch));
// Add one more to get an out of range indication (null)
pitch = pitchShift(pitch, 1);
console.log(JSON.stringify(pitch));

Answer №2

Here's a different approach to this problem:

function adjustPitch(note, octave, direction) {
    const notes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
    const index = notes.indexOf(note);
    const newIndex = (notes.length + index + (direction === 'up' ? 1 : - 1)) % notes.length;
    const newNote = notes[newIndex];

    let octaveAdjustment = 0
    if (newIndex === 0 && direction === 'up') {
        octaveAdjustment = 1
    }
    if (newIndex === notes.length - 1 && direction === 'down') {
        octaveAdjustment = -1
    }

    const updatedOctave = octave + octaveAdjustment

    return `${newNote}${updatedOctave}`
}

Answer №3

As someone with limited musical knowledge, I find that arranging the notes in the array to match the piano layout makes your code more effective for octave shifting. The other solutions seem overly complex, leaving me wondering if I overlooked something important. Nonetheless, perhaps this insight can be beneficial.

const musicalNotes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'],
      maxOctave = 4,
      minOctave = 1;

function pitchShiftNoteByHalfStep(note, octave, direction) {
  let index = musicalNotes.indexOf(note),
      newIndex;

  if ('up' === direction) {
    if (index + 1 === musicalNotes.length) {
      newIndex = 0;
      octave++;

      if (octave > maxOctave) {
        octave = maxOctave;
        console.log('Warning: Limitting to max octave.');
      }
    }
    else {
      newIndex = index + 1;
    }
  }
  else if ('down' === direction) {
    if (0 === index) {
      newIndex = musicalNotes.length - 1;
      octave--;
      
      if (octave < minOctave) {
        octave = minOctave;
        console.log('Warning: Resetting to min octave.');
      }
    }
    else {
      newIndex = index - 1;
    }
  }

  return musicalNotes[newIndex] + '' + octave;
}

Answer №4

Here's the solution I came up with for pitching shifting notes. If anyone has suggestions on how to improve it, I'd love to hear them!

export function adjustPitch(
  noteItem: Note,
  direction: ShiftDirection
): Note {
  if (validateInput(noteItem, direction)) {
    return noteItem;
  }

  const newNote = transposeNote(noteItem, direction);
  const newOctave = transposeOctave(noteItem, direction);

  return { note: newNote, octave: newOctave };
}

const transposeNote = (
  noteItem: Note,
  direction: ShiftDirection
): MusicalNote => {
  const { note } = noteItem;
  if (note === 'G#' && direction === 'up') return 'A';
  if (note === 'A' && direction === 'down') return 'G#';

  const index = allNotes.indexOf(note);
  if (direction === 'up') return allNotes[index + 1];
  return allNotes[index - 1];
};

const transposeOctave = (
  noteItem: Note,
  direction: ShiftDirection
): Octave => {
  const { note, octave } = noteItem;
  if (note === 'B' && direction === 'up') {
    return (octave + 1) as Octave;
  }
  if (note === 'C' && direction === 'down') {
    return (octave - 1) as Octave;
  }
  return octave;
};

const validateInput = (
  noteItem: Note,
  direction: ShiftDirection
): boolean => {
  const { note, octave } = noteItem;
  if (note === 'A' && octave === 0 && direction === 'down') {
    console.warn('Note is already at the lowest in the register!');
    return true;
  }
  if (note === 'G#' && octave === 7 && direction === 'up') {
    console.warn('Note is already at the highest in the register!');
    return true;
  }
  return false;
};

Reference constants and types:

type MusicalNote = (typeof allNotes)[number];
type Octave = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;

interface Note {
  note: MusicalNote;
  octave: Octave;
}

type ShiftDirection = 'up' | 'down';
const allNotes = [ 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'] as const;

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can you utilize jQuery to iterate through nested JSON and retrieve a specific matching index?

In the scenario where I have a nested JSON object like this: var library = { "Gold Rush": { "slides": ["Slide 1 Text","Slide 2 Text","Slide 3 Text","Slide 4 Text"], "bgs":["<img src='1.jpg' />","","<img src='2.j ...

Steps to successfully set up a React application without encountering any installation errors

Hey everyone, I've been trying to install React on my system for the past two days but keep encountering errors. Initially, I used the commands below to install the React app and it worked smoothly: 1. npm install -g create-react-app 2. create-react- ...

Collapse or expand nested rows within a dynamic table using Bootstrap Collapse feature

I am currently working on creating a dynamic leaderboard table for a sports league using data from a SQL database. The league consists of multiple teams, each team has different players (with some players belonging to more than one team), and players earn ...

Looking to test form submissions in React using Jest and Enzyme? Keep running into the error "Cannot read property 'preventDefault' of undefined"?

Currently, I am developing a test to validate whether the error Notification component is displayed when the login form is submitted without any data. describe('User signin', () => { it('should fail if no credentials are provided&apos ...

Retrieving online content and updating it upon reestablishing internet connection

Currently, I am in the process of developing an app that will feature a substantial amount of web content. My plan is to use Phone Gap build for its release; however, I intend to host all the content online and link to it from within the app. I have been c ...

Incorporating jQuery to seamlessly add elements without causing any disruptions to the layout

I'm looking to enhance the design of my website by adding a mouseenter function to display two lines when hovering over an item. However, I've encountered an issue where the appearance and disappearance of these lines cause the list items to move ...

Pass a selected object to a dropdown/select change event using AngularJS

Plunkr : http://plnkr.co/edit/BRQ3I4hFTlgKq4Shz19v?p=preview I'm attempting to pass the selected item from a dropdown to a function within my controller. Unfortunately, I keep receiving it as undefined. HTML : <!DOCTYPE html> <html> ...

Can one extract a property from an object and assign it to a property on the constructor?

Currently working with TypeScript, I am looking to destructure properties from an object. The challenge lies in the fact that I need to assign it to a property on the constructor of the class: var someData = [{title: 'some title', desc: 'so ...

Best practice for validating a form using React: Why the state doesn't update immediately with useState and onSubmit

I'm currently working on implementing a custom form validation for my React project using Typescript. However, I've encountered an issue with the useState hook not updating the state containing errors immediately upon form submission. Let me illu ...

Entering numerous numerical values across a variety of input fields

My website currently has a form with 6 input fields where visitors need to enter a 6 digit code. To make it easier for them, I want to allow users to simply paste the code we provide into the first input field and have the remaining digits automatically po ...

Animating a Canvas to Follow Mouse Coordinates

I am looking for a way to animate a circle moving towards specific coordinates, similar to the game . I have attempted using the jquery animate() function, but it is too slow due to the constant updating of the target coordinates. Is there a faster metho ...

Using the useState hook will help avoid any crashes when running on IE11

I recently added a new repository to my GitHub account. The file dist/index.htm is causing Internet Explorer 11 to crash, displaying the error message: "unable to get property 'root' of undefined or null reference." This issue arises when I u ...

Navigating the enum data model alongside other data model types in Typescript: Tips and Tricks

In my different data models, I have utilized enum types. Is it possible to compare the __typename in this scenario? enum ActivtiyCardType { Dance, ReferralTransaction, } type ActivityCardData = { __typename:ActivtiyCardType, i ...

disable the button border on native-base

I'm attempting to enclose an icon within a button, like so: <Button style={styles.radioButton} onPress={() => { console.log('hdjwk'); }}> <Icon ...

Guide to generating a div element with its contents using JSON

On my webpage, there is a button that increases the "counter" value every time it's clicked. I am looking to achieve the following tasks: 1) How can I generate a json file for each div on my page like the example below: <div class="text1" id="1" ...

Unable to transfer HTML code into the TinyMCE editor

I've successfully stored raw HTML content in my database, and now I want to display it in my TinyMCE editor for users to easily edit and submit. Below is the form: <textarea id="postfullOnEditPop" type="text" class="validate" placeholder="Enter f ...

Creating a personalized mouse cursor using HTML

I am trying to set an image as my cursor inside a div container but I want it to disappear and revert back to a normal pointer cursor when the mouse hovers over any link within that div. Here is my code snippet: var $box = $(".box"); var $myCursor = $ ...

generating a new item using Mongoose searches

How can I generate an object based on queries' results? The table in the meals operates using database queries. How do I handle this if the queries are asynchronous? const getQueryResult = () => { Dinner1300.count().exec(function (err, count) ...

The issue of Select2 with AJAX getting hidden behind a modal is causing

I'm currently facing an issue with Select2 within a modal. The problem can be seen here: https://gyazo.com/a1f4eb91c7d6d8a3730bfb3ca610cde6 The search results are displaying behind the modal. How can I resolve this issue? I have gone through similar ...

Potentially null object is present in a callback

The code I have is as follows: let ctx = ref.current.getContext("2d"); if(ctx){ ctx.lineWidth=1; // this line executes without errors ctx.strokeStyle=props.barStroke??"darkgray";// this line execut ...