SinterPixels Demos Part 7: Karaoke Video
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 (lineCount ≥ nextTopLineIndex) 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 (lineCount ≥ nextBottomLineIndex) 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: