6

I was wondering how would one create a function, in PHP, which is used for transposing some music chords.

I will try to explain how it works in music theory. I hope I don't forget something. If there are some mistakes, please help me to correct it.


1. The simple chords.

The simple chords are almost as simple as an alphabet and it goes like this:

C, C#, D, D#, E, F, F#, G, G#, A, A# B

From B it loops all over again to C. Therefore, If the original chord is E and we want to transpose +1, the resulting chord is F. If we transpose +4, the resulting chord is G#.

2. Expanded chords.

They work almost like the simple chords, but contain a few more characters, which can safely be ignored when transposing. For example:

Cmi, C#7, Dsus7, Emi, Fsus4, F#mi, G ...

So again, as with the simple chords, if we transpose Dsus7 + 3 = Fsus7

3. Non-root bass tone.

A problem arises when the bass plays a different tone than the chord root tone. This is marked by a slash after the chord and also needs to be transposed. Examples:

C/G, Dmi/A, F#sus7/A#

As with examples 1 and 2, everything is the same, but the part after the slash needs transpose too, therefore:

C/G + 5 = F/C

F#sus7/A# + 1 = Gsus7/B


So basically, imagine you have a PHP variable called chord and the transpose value transpose. What code would transpose the chord?

Examples:

var chord = 'F#sus7/C#';
var transpose = 3; // remember this value also may be negative, like "-4"
... code here ...
var result; // expected result = 'Asus7/E';

I have found an existed question on StackOverflow, at here. They talk about algorithm for chord-progressions.


How do I transpose music chords with PHP, by increasing or decreasing by semitones?

Matthieu Brucher
  • 21,634
  • 7
  • 38
  • 62
lvk
  • 131
  • 6
  • 1
    Welcome to SO. Please read [What topics can I ask about](http://stackoverflow.com/help/on-topic) and [How to ask a good question](http://stackoverflow.com/help/how-to-ask) And [the perfect question](http://codeblog.jonskeet.uk/2010/08/29/writing-the-perfect-question/) SO is **not a free coding or code conversion or tutorial or library finding service** You also have to show that you have made some effort to solve your own problem. – RiggsFolly Jun 06 '16 at 09:43
  • @lvk Read the links in my first comment! – RiggsFolly Jun 06 '16 at 09:51
  • 1
    It looks like you have a problem, but you haven't even tried to figure out a solution. Have you tried writing a function yet? You won't get people to write code for you here, that's not how this works. We help fix things that aren't working but you haven't made a start. Looks like you need to create an associative array like `array('C', 'C#')` etc and then you can start comparing indices in the array to get the value you want. – Jimbo Jun 06 '16 at 09:52
  • 1+2+3 - Pointless distinctions. I don't see **any** difference. It's the same task. Find the note, transpose it. – Karoly Horvath Jun 06 '16 at 10:50

3 Answers3

1

A quick solution:

<?php

// produces the expected result
echo transpose("F#sus7/C#",3);

function transpose($chord,$transpose)
{
    // the chords
    $chords = array("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B");

    $result = "";

    // get root tone
    $root_arr = explode("/",$chord);
    $root = strtoupper($root_arr[0]);

    // the chord is the first character and a # if there is one
    $root = $root[0].((strpos($root, "#") !== false)?"#":"");

    // get any extra info
    $root_extra_info = str_replace("#","",substr($root_arr[0],1)); // assuming that extra info does not have any #

    // find the index on chords array
    $root_index = array_search($root,$chords);
    // transpose the values and modulo by 12 so we always point to existing indexes in our array
    $root_transpose_index = floor(($root_index + $transpose) % 12);

    if ($root_transpose_index < 0)
    {
        $root_transpose_index += 12;
    }

    $result.= $chords[$root_transpose_index].$root_extra_info;

    if(count($root_arr)>1)
    {
        // get the non root tone
        $non_root = $root_arr[1];
        // the chord is the first character and a # if there is one
        $non_root = strtoupper($non_root[0]).((strpos($non_root, "#") !== false)?"#":"");
        // get any extra info
        $non_root_extra_info = str_replace("#","",substr($root_arr[1],1)); // assuming that extra info does not have any #

        // find the index on chords array
        $non_root_index = array_search($non_root,$chords);
        // transpose the values and modulo by 12 so we always point to existing indexes in our array
        $non_root_transpose_index = floor(($non_root_index + $transpose) % 12);

        if ($non_root_transpose_index < 0)
        {
            $non_root_transpose_index += 12;
        }

        $result.= "/".$chords[$non_root_transpose_index].$non_root_extra_info;
    }

    return $result;
}

https://3v4l.org/Cd9Pg

lots of room for improvement in code, i just tried to code it to be easy to understand.

Sharky
  • 6,154
  • 3
  • 39
  • 72
  • You could avoid some code repetition if you handled the parts of your `split` in a function. I think combining your idea to split on `/` with my `(.\#?)([^\/]*)` RegEx would get the cleanest code. – Kittsil Jun 06 '16 at 11:56
  • 1
    @Kittsil yes there is a lot of room for improvement. unfortunately, im at work atm so no more time available to work on this - i made this in my lunch break :/ at least it is easy to understand :D – Sharky Jun 06 '16 at 12:00
1

Here my regex idea with preg_replace_callback (use of anonymous function requires PHP 5.3).

function transpose($str, $t=0)
{
  // the chords
  $chords = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];

  // set transpose, return if none
  $t = (int)$t % 12 + 12; if($t % 12 == 0) return $str;

  // regex with callback
  return preg_replace_callback('~[A-G]#?~', function($m) use (&$chords, &$t) {
    return $chords[(array_search($m[0], $chords) + $t) % 12];
  }, $str);
}

Demo at eval.in   (for testing the regex pattern [A-G]#? see regex101)

echo transpose("Cmi, C#7, Dsus7, Emi, Fsus4, F#mi, G C/G, Dmi/A, F#sus7/A#", -3);

Ami, A#7, Bsus7, C#mi, Dsus4, D#mi, E A/E, Bmi/F#, D#sus7/G

bobble bubble
  • 16,888
  • 3
  • 27
  • 46
0

Okay, so there are a few things you want to handle.

First, you want to be able to loop around in the array. That's easy: use the modulus operator, which in php is %.

function transpose($chord, $increment) {
  $map = array('A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#');

  // Get the index of the given chord
  $index = array_search($chord, $map);
  if($index === false)
    return false;

  // Get the transposed index and chord
  $transpose_index = ($index + $increment) % count($map);
  if($transpose_index < 0)
    $transpose_index += count($map);
  return $map[$transpose_index];
}

Second, you want to be able to strip out the actual chords that matter to you. You can do this using a regular expression (RegEx):

function transposeFull($chords, $increment) {

  // This RegEx looks for one character (optionally followed by a sharp).
  //     .\#?
  // This RegEx looks for an optional series of characters which are not /
  //     [^\/]*
  // Put them together to get a RegEx that looks for an expanded chord
  //     (.\#?)([^\/]*)
  // Then, do it again, but add a / first, and make it optional.
  //     (\/(.\#?)([^\/]*))?
  $regex = '%(.\#?)([^\/]*)(\/(.\#?)([^\/]*))?%';

  // Note that the () allow us to pull out the matches.
  // $matches[0] is always the full thing.
  // $matches[i] is the ith match 
  //   (so $matches[3] is the whole optional second chord; which is not useful)
  $matches = array();
  preg_match($regex, $chords, $matches);

  // Then, we get any parts that were matched and transpose them.
  $chord1 = (count($matches) >= 2) ? transpose($matches[1], $increment) : false;
  $expanded1 = (count($matches) >= 2) ? $matches[2] : '';
  $chord2 = (count($matches) >= 5) ? transpose($matches[4], $increment) : false;
  $expanded2 = (count($matches) >= 6) ? $matches[5] : '';

  // Finally, put it back together.
  $chords = '';
  if($chord1 !== false)
    $chords .= $chord1.$expanded1;
  if($chord2 !== false)
    $chords .= '/'.$chord2.$expanded2;

  return $chords;
}
Kittsil
  • 2,349
  • 13
  • 22