Next up in examples of what you can do with SinterPixels, my new app in the macOS App Store: making a Karaoke Video:

This is a pretty involved script – by far the longest so far in our suite of examples. But it does a lot! It has to layout the text on the screen, apply color to the right word at the right time, fade in the new text at the right time, etc. However, it is easy enough to understand, so you can make your own karaoke videos without tinkering too much with this script. You could just replace the words and timing data in the first line, and it might word just fine if the lines for your song aren’t too long!

First, we need to have the words of the song in the script, and also we need to know how long each word should be highlighted on the screen. That’s captured in this first statement:


set allLines to { { {"Take", "me", "out", "to", "the", "ball", "game"}, {2, 1, 1, 1, 1, 3, 3} }, ¬

{ {"Take", "me", "out", "with", "the", "crowd"}, {2, 1, 1, 1, 1, 6} }, ¬

{ {"Buy", "me", "some", "pea", ".nuts", "and", "crack", ".er", "jack"}, {1, 1, 1, 1, 1, 1, 2, 1, 3} }, ¬

{ {"I", "don't", "care", "if", "I", "ev", ".er", "get", "back"}, {2, 1, 1, 1, 1, 1, 1, 1, 1} }, ¬

{ {"'cause", "it's", "root", "root", "root", "for", "the", "Phil", ".lies"}, {1, 1, 2, 1, 1, 1, 1, 3, 2} }, ¬

{ {"If", "they", "don't", "win", "it's", "a", "shame"}, {1, 2, 1, 1, 1, 1, 4} }, ¬

{ {"For", "it's", "1", "2", "3", "strikes", "you're", "out"}, {1, 1, 3, 3, 1, 1, 1, 1} }, ¬

{ {"at", "the", "old", "ball", "game"}, {1, 1, 3, 3, 6} } }


Here we have a list of eight items. each item is a pair of lists – a list of “words” and a list of durations for each word. If you look carefully, not all of the words are words. In the case we have a two-syllable word, like “peanuts”, we want the two syllable highlighted separately, so we make it two entries – “pea” and “.nuts” The period in from of “nuts” is the signal to ourselves that we don’t want a space between “pea” and “nuts” in the layout.

In the list of durations, a 1 corresponds to a quarter note, 2 to a half note, etc. “Take Me Out to The Ball Game” is in ¾ time, so a line of 4 bars generally adds up to 12, but in a few verses, the next line begins before the 4 bars finish, so that isn’t always the case.

One of our helper functions is durationOfLine(karaokeLine). In this case, karaokeLine is the pair of list of words and list of durations. This function just adds up the durations:


to durationOfLine(karaokeLine)

set ticks to item 2 of karaokeLine

set lineDuration to 0

repeat with t in ticks

set lineDuration to lineDuration + t

end repeat

return lineDuration

end durationOfLine


Another helper function is removeFirstCharOf(str). This one is useful to pull that leading period off of a string like “.nuts”


to removeFirstCharOf(str)

set newString to (characters 2 thru -1 of str) as string

return newString

end removeFirstCharOf


An important helper function is lineLayout(karaokeLine, yPos, theDoc, wordColor) This function creates and returns a list of text shape objects at the given y coordinate. Each text shape has the right x coordinate, allowing for spaces between the words.


to layoutLine(karaokeLine, yPos, theDoc, wordColor)

set wds to item 1 of karaokeLine

set lineLayout to {}

set specialWordIDs to {}

set totalSize to 0

repeat with wthText in wds

set isSpecial to wthText begins with "."

if isSpecial then

set additionalSpace to 0

set displayText to removeFirstCharOf(wthText)

else

set additionalSpace to 10

set displayText to wthText

end if

tell application "SinterPixels"

tell theDoc

set wthWord to make new text shape with properties {text content:displayText, position:{0, yPos}, fill color:wordColor, font size:32}

set wthSize to size of wthWord

set totalSize to totalSize + (width of wthSize) + additionalSpace

set lineLayout to lineLayout & {wthWord}

if isSpecial then

set specialWordIDs to specialWordIDs & ID of wthWord

end if

end tell

end tell

end repeat

-- now space out the words on the line

set currentx to totalSize / -2

repeat with wthWord in lineLayout

tell application "SinterPixels"

tell theDoc

if specialWordIDs does not contain ID of wthWord then

set currentx to currentx + 10

end if

set thisWidth to width of size of wthWord

set x coordinate of wthWord to (currentx + thisWidth / 2)

set currentx to currentx + thisWidth

end tell

end tell

end repeat

return lineLayout

end layoutLine


Part of the script will make the words fade in , with the helper function wax() – and make the words fade out, with the helper function fade(). The fade in and fade out is accomplished by setting the alpha of the text shapes.


to fade(wordIds, theDoc)

set remainingIDs to {}

repeat with wthId in wordIds

tell application "SinterPixels"

tell theDoc

set oldAlpha to alpha component of fill color of text shape id wthId

try

if oldAlpha > 0.06 then

set alpha component of fill color of text shape id wthId to oldAlpha - 0.04

set remainingIDs to remainingIDs & wthId

else

delete text shape id wthId

end if

end try

end tell

end tell

end repeat

return remainingIDs

end fade


to wax(waxingWordData, theDoc)

set newWaxingData to {}

repeat with nthWaxDatum in waxingWordData

set theID to shapeID of nthWaxDatum

set newAlpha to (alf of nthWaxDatum) + 0.01

if newAlpha ≥ 1.0 then

set newAlpha to 1.0

end if

if newAlpha > 0.0 then

tell application "SinterPixels"

tell theDoc

set the alpha component of fill color of shape id theID to newAlpha

end tell

end tell

end if

if newAlpha < 1.0 then

set newWaxingData to newWaxingData & { {shapeID:theID, alf:newAlpha} }

end if

end repeat

return newWaxingData

end wax


And the last helper function – fillNewWaxingWordData(waxingData, lineLayout), which sets up the text shapes so they are ready to fade in:


to fillNewWaxingWordData(waxingData, lineLayout)

set newWaxingWordData to waxingData

set wordAlpha to -1.0

repeat with nthWord in lineLayout

set newWaxingWordData to newWaxingWordData & { {shapeID:id of nthWord, alf:wordAlpha} }

set wordAlpha to wordAlpha - 0.25

end repeat

return newWaxingWordData

end fillNewWaxingWordData


Now the part of the script that puts all the pieces together. It starts a movie, proceeds to layout each line at the right time, and invokes the fade in and fade out of each text shape. When the time is right, it sets the color of one of the words to blue, and resets the previously blue word to gray Most importantly, it records each movie frame, and when the movie is complete, save the movie to a file.


set lineCount to count allLines

set fadingWordIDs to {}

set waxingWordData to {}

set lineCount to count allLines

tell application "SinterPixels"

set theDoc to make new document with properties {height:112, width:600}

tell theDoc

start filming

set numLoops to (lineCount + 1) / 2

set topLine to item 1 of allLines

set topLineLayout to my layoutLine(topLine, 28, theDoc, {0.5, 0.5, 0.5, 1.0})

set bottomLine to item 2 of allLines

set bottomLineLayout to my layoutLine(bottomLine, -28, theDoc, {0.5, 0.5, 0.5, 1.0})

repeat with nthLoop from 1 to numLoops

set nextTopLineIndex to (2 * nthLoop + 1)

if (lineCountnextTopLineIndex) then

set nextTopLine to item nextTopLineIndex of allLines

set nextTopLineLayout to my layoutLine(nextTopLine, 28, theDoc, {0.5, 0.5, 0.5, 0.0})

set waxingWordData to my fillNewWaxingWordData(waxingWordData, nextTopLineLayout)

else

set nextTopLine to {}

set nextTopLineLayout to {}

end if

set ticks to item 2 of topLine

set w to 0

repeat with wthWord in topLineLayout

set w to w + 1

set wthCount to item w of ticks

set frames to wthCount * 15

set fill color of wthWord to blue

repeat with fr from 1 to frames

record movie frame duration 1

set fadingWordIDs to my fade(fadingWordIDs, theDoc)

set waxingWordData to my wax(waxingWordData, theDoc)

end repeat

set fadingWordIDs to fadingWordIDs & ID of wthWord

set fill color of wthWord to gray

end repeat

if (nthLoop > 1) then

set bottomLine to nextBottomLine

set bottomLineLayout to nextBottomLineLayout

end if

set nextBottomLineIndex to (2 * nthLoop + 2)

if (lineCountnextBottomLineIndex) then

set nextBottomLine to item nextBottomLineIndex of allLines

set nextBottomLineLayout to my layoutLine(nextBottomLine, -28, theDoc, {0.5, 0.5, 0.5, 0.0})

set waxingWordData to my fillNewWaxingWordData(waxingWordData, nextBottomLineLayout)

else

set nextBottomLineLayout to {}

set nextBottomLine to {}

end if

set w to 0

set ticks to item 2 of bottomLine

repeat with wthWord in bottomLineLayout

set w to w + 1

set wthCount to item w of ticks

set frames to wthCount * 15

set fill color of wthWord to blue

repeat with fr from 1 to frames

record movie frame duration 1

set fadingWordIDs to my fade(fadingWordIDs, theDoc)

set waxingWordData to my wax(waxingWordData, theDoc)

end repeat

set fadingWordIDs to fadingWordIDs & ID of wthWord

set fill color of wthWord to gray

end repeat

set topLineLayout to nextTopLineLayout

set topLine to nextTopLine

end repeat

repeat with fr from 1 to 120

record movie frame duration 1

set fadingWordIDs to my fade(fadingWordIDs, theDoc)

end repeat

stop filming filename "KaraokeMovie.mov"

end tell

end tell


See the SinterPixels support github site for additional info:

Here’s the full video on YouTube:

www.youtube.com/watch