replacing chords blues

I am trying to replace each chord in variable thisLine (“. DÃ…dim Fsus4 CÃ… D C G DÃ…dim”) with each item of variable newChords: ({“Edim”, “F#sus4”, “D”, “D#”, “C#”, “G#”, “Edim”})
I need to maintain the exact spacing as these new chords need to be placed exactly where the old one was.
As you can see below the offset function below fails to space correctly.
Any idea how to correct this?
Thanks

set oldChords to {"DÃ…dim", "Fsus4", "CÃ…", "D", "C", "G", "DÃ…dim"}
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}
set thisLine to ".      DÃ…dim               Fsus4          CÃ…             D          C          G         DÃ…dim"
set newLine to {}
set cntr to (count newChords)
repeat with i from 1 to cntr
	set oldChord to (word i of thisLine)
	set newChord to item i of newChords
	set theoff to (text 1 through ((offset of oldChord in thisLine) - 1)) of thisLine
	if oldChord's length > newChord's length then -- a sharp became a note
		set theoff to theoff & " "
	else
		if oldChord's length > newChord's length then set theoff to items 2 through end of theoff
	end if
	set theLength to (theoff's length)
	if i > 1 then
		set theLength to theLength - ((newLine as text)'s length)
	end if
	repeat with j from 1 to theLength -- (theoff's length)
		set end of newLine to " "
	end repeat
	set end of newLine to newChord
end repeat
set newLine to "." & items 2 through end of newLine as text
return thisLine's length & " " & newLine's length & " " & newLine
-- returned {94, " ", 85, " ", ".       Edim               F#sus4          DD#C#                               G#Edim"}

You don’t say what the spacing element is.

Hi Adam,

Thanks for helping.

They are all spaces, i.e. spacebar presses to be used with a mono spaced font like monaco. The spaces vary in number in between chords.

The lyrics go in the line below each chord line and the new chords need to be at the exact spot where the old ones were so:
. DÃ…dim Fsus4 CÃ… D C G DÃ…dim
Ma che piccola storia ignobile mi vieni a raccontare, così solita e banale come tante,

needs to become
. Edim F#sus4 D D# C# G# Edim
Ma che piccola storia ignobile mi vieni a raccontare, così solita e banale come tante,

I hope this answers.

Hi.

Vanilla:

set thisLine to ".      DÃ…dim               Fsus4          CÃ…             D          C          G         DÃ…dim"
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set newLine to thisLine's text items

set j to 0
repeat with i from 2 to (count newLine)
	if ((count item i of newLine) > 0) then
		set j to j + 1
		set item i of newLine to item j of newChords
	end if
end repeat

set newLine to newLine as text
set AppleScript's text item delimiters to astid
newLine

Satimage OSAX:

set thisLine to ".      DÃ…dim               Fsus4          CÃ…             D          C          G         DÃ…dim"
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "( +)"
set searchStr to ".( *)" & thisLine's words
set AppleScript's text item delimiters to astid

set replaceStr to "."
repeat with i from 1 to (count newChords)
	set replaceStr to replaceStr & ("\\" & i & item i of newChords)
end repeat
--return result
set newLine to (change searchStr into replaceStr in thisLine with regexp)

There should be something similar to the Satimage version for a “sed” shell script, but I haven’t been able to make it work. :expressionless:

Presumably, since your’e apparently just transposing the line up a semitone, the "Ã…"s in the original line should have been "#"s.

Hi Nigel,
Thanks for the 2 great scripts.

Yes it should but I found out that # was taken as a word delimiter and had to change it to something else and reswap it to # at the end of the job.


word 1 of ".    F#dim" -- returns "F"
word1 of ".    FÃ…dim" -- returns "FÃ…dim"

Your scripts transpose correctly and they get me closer than ever to completion.
However when the chords are spaced with a random number of spaces among them they fail to position correctly.
I tested with the following line with both scripts:
“. DÃ…dim Fsus4 CÃ… D C G DÃ…dim”
there are
6 spaces between . and DÃ…dim
15 spaces between DÃ…dim and Fsus4
10 spaces between Fsus4 and CÃ…
13 spaces between CÃ… and D
10 spaces between D and C
10 spaces between C and G
9 spaces between G and DÃ…dim
The line that I get is different in length from the original.
. Edim F#sus4 D D# C# G# Edim
. DÃ…dim Fsus4 CÃ… D C G DÃ…dim
F#sus4 is longer than Fsus4, for example, and the spacing does not change to accommodate it.
This will place some chords on a different letter of the words.
What I am trying to achieve is to tell the musician exactly on what character of the word to change chord.
This is what I was trying to do with this code:

 if oldChord's length > newChord's length then -- a sharp became a note
       set theoff to theoff & " "
   else
       if oldChord's length > newChord's length then set theoff to items 2 through end of theoff
   end if
   set theLength to (theoff's length)

I hope this make sense :slight_smile:

are oldChords and newChords always the same chords and in the same order as in thisLine or should oldChords and newChords be a replacement table? Do the “dim” and “sus4” parts ever change?

Hmmm. You can’t sensibly do that unless you’re using a mono-spaced font. But if you are, this is one way to get the replacement chords starting at the same character offsets into the line:


set thisLine to ".      D#dim               Fsus4          C#             D          C          G         D#dim" -- This does have the right number of spaces if you use the "Open this scriplet" link!
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set newLine to thisLine's text items

set j to 0
repeat with i from 2 to (count newLine)
	set thisTI to item i of newLine
	if ((thisTI's class is text)) and ((count thisTI) > 0) then
		set j to j + 1
		set replaceStr to item j of newChords
		set lengthDifference to (count replaceStr) - (count thisTI)
		if (lengthDifference < 0) then
			-- Original longer than replacement. Pad the replacement with spaces.
			repeat -lengthDifference times
				set replaceStr to replaceStr & space
			end repeat
		else
			-- Replacement longer or same length. Zap following ""s (which represent spaces).
			repeat with k from (i + 1) to (i + lengthDifference)
				set item k of newLine to missing value
			end repeat
		end if
		set item i of newLine to replaceStr
	end if
end repeat

-- Coerce the list items which are still class text back to a single text using the space delimiter.
set newLine to newLine's text as text
set AppleScript's text item delimiters to astid
newLine

Just for fun, this is a pure musical solution to transpose chords.
But there is a crucial and unhandy limitation: the handler considers only sharp alterations (#),
it fails if the chord contains any B flat


property halftones : {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

set transposedChord to transpose from "Fsus4" by 6 --> Bsus4

on transpose from chord by interval
	set suffix to ""
	set chordCount to count chord
	set main to character 1 of chord
	if chordCount > 1 then
		if character 2 of chord is "#" then
			set main to text 1 thru 2 of chord
			if chordCount > 2 then set suffix to text 3 thru -1 of chord
		else
			set suffix to text 2 thru -1 of chord
		end if
	end if
	set chordIndex to indexOfItemInHalftones(main) + 11
	set newMain to item (((chordIndex + interval) mod 12) + 1) of halftones
	return newMain & suffix
end transpose

on indexOfItemInHalftones(theItem)
	repeat with i from 1 to count halftones
		if theItem is item i of halftones then return i
	end repeat
end indexOfItemInHalftones


Hi Nigel,

Thanks for alerting me to the “Open this scriplet” link. I did not know that.

You are right. In fact I read a plain text file with the song and chords in it and I write another file with the chords transposed. TextEdit uses Monaco by default so I am ok there.

That last code is just about perfect: Gratitude!
A minor glitch: It adds 1 space at the end of the chords. (easily fixed and only worrying in the case that it exceeds the max number of chars for that line and TextEdit forces the line to wrap to the next).

I could make it fail only once when I eliminated the space in between the . and the first chord D#dim.
It then did not transpose the first chord and it never listed the last one: D#dim
– result ".D#dim Edim F#sus4 D D# C# "
This could in fact happen when one tries to squeeze as many chords as one can on the one line.
See below:


set thisLine to ".D#dim  Fsus4   C#    D     C      G       D#dim" -- This does have the right number of spaces if you use the "Open this scriplet" link!

set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set newLine to thisLine's text items

set j to 0
repeat with i from 2 to (count newLine)
	set thisTI to item i of newLine
	if ((thisTI's class is text)) and ((count thisTI) > 0) then
		set j to j + 1
		set replaceStr to item j of newChords
		set lengthDifference to (count replaceStr) - (count thisTI)
		if (lengthDifference < 0) then
			-- Original longer than replacement. Pad the replacement with spaces.
			repeat -lengthDifference times
				set replaceStr to replaceStr & space
			end repeat
		else
			-- Replacement longer or same length. Zap following ""s (which represent spaces).
			repeat with k from (i + 1) to (i + lengthDifference)
				set item k of newLine to missing value
			end repeat
		end if
		set item i of newLine to replaceStr
	end if
end repeat

-- Coerce the list items which are still class text back to a single text using the space delimiter.
set newLine to newLine's text as text
set AppleScript's text item delimiters to astid
newLine

I just finished fixing it as best as I could. I now works with the chords next to the . as well as with a space in between. Could it be done better?


set thisLine to ". D#dim  Fsus4   C#    D     C      G       D#dim" -- This does have the right number of spaces if you use the "Open this scriplet" link!
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}
-- if we find that the first chord is joined to the . (no space in between)
if item 2 of thisLine is not " " then
	-- fix it so that now there is a space in between them
	set thisLine to item 1 of thisLine & space & items 2 thru end of thisLine
	set mustRedo to true -- we must fix it later
else
	set mustRedo to false -- we have nothing to do later
end if

set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set newLine to thisLine's text items

set j to 0
repeat with i from 2 to (count newLine)
	set thisTI to item i of newLine
	if ((thisTI's class is text)) and ((count thisTI) > 0) then
		set j to j + 1
		set replaceStr to item j of newChords
		set lengthDifference to (count replaceStr) - (count thisTI)
		if (lengthDifference < 0) then
			-- Original longer than replacement. Pad the replacement with spaces.
			repeat -lengthDifference times
				set replaceStr to replaceStr & space
			end repeat
		else
			-- Replacement longer or same length. Zap following ""s (which represent spaces).
			repeat with k from (i + 1) to (i + lengthDifference)
				-- I had to modify this line because in one of my tests it failed trying to set item k which did not exist
				if k ≤ newLine's length then set item k of newLine to missing value
			end repeat
		end if
		set item i of newLine to replaceStr
	end if
end repeat
-- if necessary reput the chord next to the .
if mustRedo then set part1 to item 1 of newLine & item 2 of newLine & space
-- Coerce the list items which are still class text back to a single text using the space delimiter.
set newLine to newLine's text as text
if mustRedo then
	-- find out where we must start to append part 2 of newLine
	set part2Start to ((text 1 through ((offset of (item 2 of newLine) in newLine as text))) of newLine as text)'s length
	-- join the corrected chord part to the rest of the newLine
	set newLine to part1 & text items part2Start through end of newLine
end if
-- if we are left with a trailing space, get rid of it
if (last item of newLine) is space then set newLine to items 1 thru -2 of newLine

set AppleScript's text item delimiters to astid
-- Coerce again
set newLine to newLine's text as text
newLine

At 1 am I found that if you change thisLine to:

set thisLine to ".D               F          C             D          C          G         D"

and newChords to:

set newChords to {"D#", "F#", "C#", "D#", "C#", "G#", "D#"}

it fails miserably and returns:
“.D# D# F# C# D# C# G# D#”
One extra D#. If you insert a space between the . and the first chord D it then still works.
Is it possible to write some code that will work for all chords? I start to wander!
I am sad and tired. I am going to bed.
See you guys tomorrow.

Hi Stefan,

Thanks for that.
I know you are into music and I much appreciate your help.

Unfortunately your code brought up a Syntax error: Expected end of line, etc. but found “from”.
Software or system incompatibility maybe?
I run an iMac 3.06 Intel Core 2 Duo with 4 GB 800 MHz DDR2 SDRAM, Lion 10.7.3, AppleScript editor 2.4.2(131) Applescript 2.2.1
it worked like this though and I was very happy to find out that you can give it negative numbers less than 12 and also bigger than 12 without glitches.
You rock!
Idea: It would be nice to display how may octaves up or down we transposed to: Bsus4Up2. Maybe I’ll work on it :slight_smile:



property halftones : {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

set transposedChord to transpose {"Fsus4", 6} --> Bsus4

on transpose {chord, interval}
	set suffix to ""
	set chordCount to count chord
	set main to character 1 of chord
	if chordCount > 1 then
		if character 2 of chord is "#" then
			set main to text 1 thru 2 of chord
			if chordCount > 2 then set suffix to text 3 thru -1 of chord
		else
			set suffix to text 2 thru -1 of chord
		end if
	end if
	set chordIndex to indexOfItemInHalftones(main) + 11
	set newMain to item (((chordIndex + interval) mod 12) + 1) of halftones
	return newMain & suffix
end transpose

on indexOfItemInHalftones(theItem)
	repeat with i from 1 to count halftones
		if theItem is item i of halftones then return i
	end repeat
end indexOfItemInHalftones



It’s sloppy, but the extra space is just the padding when the last chord name is replaced with one that’s one character shorter. The number of characters in the line remains the same. However, if the last name’s replaced with a longer one, there will be extra characters in the line. But then again, my original script doesn’t cater for longer last names and errors on them. :rolleyes:

¨¨

Right. I hadn’t guessed that was a possibility. My own fix is below:

  1. It works with or without space(s) after the initial dot, but the initial dot is still assumed to be a given.
  2. Padding isn’t added after the last replacement chord in the line.
  3. A longer chord name at the end of the line no longer crashes the script.
  4. In case the chords are tightly packed into the line, space zapping after longer names is now more circumspect.
  5. There still remains the problem of what adjustments to make if longer chord names displace the end of the line or the following chord positions.

set thisLine to ".      D#dim               Fsus4          C#             D          C          G         D#dim" -- This does have the right number of spaces if you use the "Open this scriplet" link!
set newChords to {"Edim", "F#sus4", "D", "D#", "C#", "G#", "Edim"}


set padding to "                                                                                " -- 80 (say) spaces. (Ditto "Open this scriplet"!)
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set TIs to text items of (text 2 thru -1 of thisLine)

set j to 0
set TICount to (count TIs)
repeat with i from 1 to TICount
	set thisTI to item i of TIs
	if ((thisTI's class is text) and ((count thisTI) > 0)) then
		-- This text item's a chord name. Get the next replacement name from the list.
		set j to j + 1
		set replaceStr to item j of newChords
		-- Compare the lengths of the two names.
		set lengthDifference to (count replaceStr) - (count thisTI)
		if ((lengthDifference < 0) and (i < TICount)) then
			-- The replacement's shorter than the original and will not come at the end of the line. Pad it with spaces.
			set replaceStr to replaceStr & (text lengthDifference thru -1 of padding)
		else
			-- The replacement's longer than the original or same length. Zap the appropriate number of following ""s (representing spaces) ” if there are enough!
			set zapEndIndex to (i + lengthDifference)
			if (zapEndIndex ≤ TICount) then -- Zapping won't run past the end of the list.
				repeat with k from (i + 1) to zapEndIndex
					if ((count item k of TIs) > 0) then exit repeat -- Don't zap the next chord!
					set item k of TIs to missing value
				end repeat
			end if
		end if
		-- Substitute the replacement for the original in the text item list.
		set item i of TIs to replaceStr
	end if
end repeat

-- Coerce the list items which are still text back to a single text using the space delimiter and prepend the intial ".".
set newLine to "." & TIs's text
set AppleScript's text item delimiters to astid
newLine

Lovely. It’s not quite Finale, but it’s a start! :lol:

Hi Nigel,

I woke up to a nice surprise!
I am testing right now but this time we should be ok.
Thanks again for your help.

Hi Stefan,

I think I got it for maintaining flats or sharps as found on the original line. (I am not looking at spacing here, just the chords)
What do you think? Find any problems?


property halftonesSharp : {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}
property halftonesFlat : {"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"}

set substituteSharp to "Ã…" -- because Applescript consideres # a word delimiter

set thisLine to "Csus4   Dbdim   C#  Bb"
if thisLine contains "#" then set thisLine to searchReplaceAccidental("#", substituteSharp, thisLine)

set newChords to {}
set interval to -3
set cntr to count words of thisLine
repeat with k from 1 to cntr
	set thisChord to word k of thisLine
	if thisChord contains substituteSharp then
		set thisChord to searchReplaceAccidental(substituteSharp, "#", thisChord)
		set transposedChord to transpose {thisChord, interval, halftonesSharp}
	else
		set transposedChord to transpose {thisChord, interval, halftonesFlat}
	end if
	set end of newChords to transposedChord
end repeat
newChords -- returns {"Asus4", "Bbdim", "A#", "G"}

on transpose {chord, interval, theHalfTones}
	set suffix to ""
	set chordCount to count chord
	set main to character 1 of chord
	if chordCount > 1 then
		-- changed to allow flats
		if character 2 of chord is "#" or character 2 of chord is "b" then
			set main to text 1 thru 2 of chord
			if chordCount > 2 then set suffix to text 3 thru -1 of chord
		else
			set suffix to text 2 thru -1 of chord
		end if
	end if
	set chordIndex to indexOfItemInHalftones(main, theHalfTones) + 11
	set newMain to item (((chordIndex + interval) mod 12) + 1) of theHalfTones
	return newMain & suffix
end transpose

on indexOfItemInHalftones(theItem, theHalfTones)
	repeat with i from 1 to count theHalfTones
		if theItem is item i of theHalfTones then return i
	end repeat
end indexOfItemInHalftones

on searchReplaceAccidental(searchAccidental, replaceAccidental, theChord)
	set searchAccidental to searchAccidental as list
	set replaceAccidental to replaceAccidental as list
	set theChord to theChord as text
	
	set oldTID to AppleScript's text item delimiters
	
	repeat with i from 1 to count searchAccidental
		set AppleScript's text item delimiters to searchAccidental's item i
		set theChord to theChord's text items
		set AppleScript's text item delimiters to replaceAccidental's item i
		set theChord to theChord as text
	end repeat
	
	set AppleScript's text item delimiters to oldTID
	
	return theChord
end searchReplaceAccidental

nice :slight_smile:

Yeah. Not bad! :slight_smile:

I thought I’d have a go at writing a chord-transposition handler which takes a named interval instead of a semitone shift. This just does one chord at a time:


on transposeChord(originalChord, interval, direction)
	set baseWidths to {0, 2, 4, 5, 7, 9, 11} -- Widths of perfect and major intervals in semitones.
	set allEnharmonics to {{"B#", "C", "Dbb"}, {"Bx", "C#", "Db"}, {"Cx", "D", "Ebb"}, {"D#", "Eb", "Fbb"}, {"Dx", "E", "Fb"}, {"E#", "F", "Gbb"}, {"Ex", "F#", "Gb"}, {"Fx", "G", "Abb"}, {"G#", "Ab"}, {"Gx", "A", "Bbb"}, {"A#", "Bb", "Cbb"}, {"Ax", "B", "Cb"}}
	
	-- Parse the chord parameter for note and chord type.
	if ((count originalChord) is 1) or ((character -1 of originalChord is in "x#b") and (originalChord does not contain "/")) then
		set {originalNote, chordType} to {originalChord, ""}
	else
		set i to 2
		repeat while (character i of originalChord is in "x#b")
			set i to i + 1
		end repeat
		set {originalNote, chordType} to {text 1 thru (i - 1) of originalChord, text i thru -1 of originalChord}
	end if
	-- Recursively deal with any slash in the chord type.
	set i to (offset of "/" in chordType)
	if (i > 0) then set chordType to text 1 thru i of chordType & transposeChord(text (i + 1) thru -1 of chordType, interval, direction)
	
	-- Parse the interval parameter for quality and number.
	set {intervalQuality, intervalNumber} to words of interval
	-- Get the interval number as a number from 1 to 7.
	if (intervalNumber is in "unison octave") then
		set intervalNumber to 1
	else if (((count intervalNumber) > 2) and (text -2 thru -1 of intervalNumber is in "st nd rd th ve va")) then
		set intervalNumber to text 1 thru -3 of intervalNumber
	end if
	set intervalNumber to (intervalNumber - 1) mod 7 + 1
	
	-- Get the transposition distance in semitones.
	set semitoneShift to item intervalNumber of baseWidths
	if (intervalQuality begins with "aug") then
		set semitoneShift to semitoneShift + 1
	else if (intervalQuality begins with "min") then
		set semitoneShift to semitoneShift - 1
	else if (intervalQuality begins with "dim") then
		if ((intervalNumber is 1) or (intervalNumber is 4) or (intervalNumber is 5)) then
			set semitoneShift to semitoneShift - 1
		else
			set semitoneShift to semitoneShift - 2
		end if
	end if
	
	-- If the interval direction is down, invert the number and semitone values.
	if (direction is "down") then
		set intervalNumber to (9 - intervalNumber)
		set semitoneShift to (12 - semitoneShift)
	end if
	
	-- Get the index of the enharmonic group containing the original note.
	set i to 1
	repeat until (item i of allEnharmonics contains originalNote)
		set i to i + 1
	end repeat
	-- Get the enharmonic group which is the relevant number of semitones away from it.
	set targetEnharmonics to item ((i + semitoneShift + 11) mod 12 + 1) of allEnharmonics
	-- Calculate the index in this second group of the note with the required letter.
	set j to ((id of character 1 of originalNote) + intervalNumber - (id of character 1 of beginning of targetEnharmonics) + 6) mod 7 + 1
	-- In extreme cases, the transposed note may be a triple sharp or flat. Rationalise it.
	if (j > (count targetEnharmonics)) then
		if (originalNote ends with "b") then
			set j to -1
		else
			set j to 1
		end if
	end if
	
	-- Return the full transposed chord.
	return (item j of targetEnharmonics) & chordType
end transposeChord


set l to {}
repeat with thisChord in {"Csus4", "Dbdim", "B", "C#", "Bb/A"}
	set end of l to transposeChord(thisChord, "major 3rd", "up")
end repeat
l --> {"Esus4", "Fdim", "D#", "E#", "D/C#"}

Edit: 1. Fixed a bug which caused an error in some situations. 2. Combined the calculation of the transposed note letter and the matching of it in the target enharmonic group into a single, reduced calculation. 3. Notional triple sharps and flats are now rationalised as simpler enharmonics instead of just being returned as ‘false’.