Alphabet Project, Part 2
Previous entries in this series:
Welcome back! Thanks for coming back! I know I promised we’d start off with turning some numbers into music, but I wanted to take a quick detour back into generating those numbers.
Step 2, Part Deux
By the end of the last post I’d generated a set of graph coordinates for the letter A
,
starting with
and ending up with a JSON array that looked, in part, like this:
[
[0, 30], [1, 30], [2, 30], [3, 30], [4, 30], [5, 30], [6, 30], [7, 30], [8, 30], [9, 30], [10, 30],
[11, 30], [12, 30], [13, 30], [14, 30], [15, 30], [16, 30], [17, 30], [18, 30], [19, 30], [20, 30],
[21, 30], [22, 30], [23, 30], [24, 30], [25, 30], [26, 30], [27, 30], [28, 30], [29, 30], [30, 30],
[31, 31], [32, 31], [33, 31], [34, 31], [35, 32], [36, 32], [37, 32], [38, 32], [39, 33], [40, 33],
[41, 33], [42, 33], [43, 33], [44, 34], [45, 34], [46, 35], [47, 35], [48, 36], [49, 36], [50, 37],
...
]
I said that we had to do this 25 more times, which is true, but after about 3 times, I noticed a few things.
First, it’s really kind of annoying to open the Processing file, change the letter "a"
to "b"
every time, rerun it,
realize I missed one, and then have to go back and redo "a"
because I overwrote some of the data.
Second, because of the not-particularly-scientific way I created the individual letter chart images, they’re not all
the same height, meaning I need to find the dimensions of the image before I start.
Using imagemagick
this is at least an easy task
> identify data/a.png
data/a.png PNG 202x49 202x49+0+0 8-bit sRGB 11.1KB 0.000u 0:00.000
And then I just need to open the Processing code, update the sketch dimensions, double check that I’ve changed all instances of the letter character to the correct one, and…
Wait a minute! A series of easily performable but repetitive tasks? This sounds like a job for
A script!
I’ve been getting really into Elixir, so let’s use that.
Fortunately for us, Processing now includes a command line option, processing-java
that takes a path to our sketch’s directory, a flag to --run
or --build
, and any
number of arguments.
Ideally, we’d want to run something like
processing-java --sketch=/path/to/our/graph/detector --run <letter> <width> <height>
In Elixir, we can sequence our calls to identify
and processing-java
to do just that:
defmodule CoordinateDetector do
def process!(letter) do
IO.write("Processing #{letter}...")
dimensions = get_dimensions(letter)
run_processing(letter, dimensions)
IO.puts("Done")
end
defp get_dimensions(letter) do
# Run imagemagick's `identify` and store the returned string
{str, 0} = System.cmd("identify", ["data/#{letter}.png"])
# Hooray pattern matching!
# `Regex.scan/2` returns an array of arrays, where each inner array
# is of the form [full_match | grouped_matches].
# Since we want the two dimensions, we can match on
# the second and third elements of the first array
# and ignore everything else
[[_, w, h] | _] = Regex.scan(~r/(\d+)x(\d+)/, str)
# Return the dimensions as a tuple
{w, h}
end
defp run_processing(letter, {w, h}) do
# We'll be running this from the location of the Processing sketch,
# so we can use `System.cwd/0` and not need to hard code our fs location
System.cmd("processing-java", [
"--sketch=#{System.cwd()}",
"--run", letter, w, h
])
end
end
Then, in our processing sketch, we just need to make a couple small changes:
str letter;
str pngFilename;
void settings() {
size(int(args[1]), int(args[2]));
}
void setup() {
...
letter = args[0];
// now we can use this anywhere we need to save an image.
pngFilename = letter + ".png";
}
Next we add an Elixir function to iterate through the alphabet
defmodule CoordinateDetector do
@letters ~w(a b c d e f g h i j k l m n o p q r s t u v w x y z)
def process_all! do
Enum.each(@letters, fn letter ->
process!(letter)
end)
end
...
end
And let’s see how this goes:
iex(1)> :timer.tc(CoordinateDetector, :process_all!, [])
Processing a...Done
Processing b...Done
Processing c...Done
...
Processing z...Done
{71535134, :ok}
That time is in microseconds, so we’re looking at 71 seconds. Not bad, but since we’re using Elixir, we might as well use one of its great features: spawning processes to do work in different, independent threads.
A few changes to the Elixir code later
defmodule CoordinateDetector do
...
def process_all! do
# spawn a new process to generate data for each letter
# `Enum.map/2` here returns a list of PIDs...
Enum.map(@letters, fn letter ->
spawn(__MODULE__, :process!, [letter])
end)
# and we send a `:check` message to each PID, along with the PID of
# the main process so we can send a response back
|> Enum.map(fn pid ->
send pid, {self(), :check}
end)
# start waiting for responses
start_receiving(0)
end
def start_receiving(n) do
# If we haven't received a response for all the letters...
if n < length(@letters) do
receive do
# when we get a response back...
{_pid, message} ->
# print out the message and
IO.puts message
# call this function again with an incremented count
start_receiving(n + 1)
end
# otherwise return `:ok`
else
{:ok, n}
end
end
def process!(letter) do
receive do
# when the process receives `:check`
{sender, :check} ->
# run our Processing code
IO.puts("Processing #{letter}...")
dimensions = get_dimensions(letter)
run_processing(letter, dimensions)
# and send a done message back to the main thread
send sender, {self(), "done processing #{letter}"}
end
end
...
end
and we run the code again
iex(1)> :timer.tc(CoordinateDetector, :process_all!, [])
Processing a...
Processing b...
Processing c...
...
Processing z...
Done processing x
Done processing a
Done processing r
...
Done processing o
{33144238, {:ok, 26}}
33 seconds! Much better. As you can see, we’re not concerned about the order the processes complete in, since they’re entirely independent of each other.
And so, 33 seconds later, we’ve got 26 JSON files, each with a set of coordinates. Now we can move on to the music!
Thanks for reading! If you liked this post, and want to know when the next one is coming out, follow me on Twitter (link below)!