Running Coach (#82)

The three rules of Ruby Q.:

  1. Please do not post any solutions or spoiler discussion for this quiz
    until
    48 hours have passed from the time on this message.

  2. Support Ruby Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

  1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Benjohn B.

I’ve started to jog with my girlfriend. It’s hell. We’re following a
“programme”
at this web site:

http://www.coolrunning.com/engine/2/2_3/181.shtml

The aim is to get you from being able to alternate hobbling and brisk
walking,
to being able to jog for 20 minutes solidly. Over eight weeks you
exercise for
twenty minutes, three times a week. Over the eight weeks, the ratio of
jog to
walk steadily increases, and the jogs get longer, while the walks become
shorter.

I was explaining to a friend that it’s incredibly difficult for me to
look at a
stop watch and work out in my head if we’re supposed to be jogging or
walking,
how many more jogs we’ve got to do, and when I can stop and rest. He
suggested:
‘why not tape yourself giving prompts about when to start and stop’. A
brilliant
plan. ‘Even better, record it on to your phone’. Genius! Except I’m the
kind of
person who’s lazy enough to spend eight times as long writing a program
to try
to do this for me.

So, the quiz is:

Write a program to create the tracks for each of the eight weeks. Make
it give
helpful and enthusiastic advice like “you’ve got to run for another
minute / 30
seconds / 15 seconds …”, “walk now for two minutes, you’ve got three
jogs
left”, “you’re on jog 2 of 6”, or “well done, that’s your last jog.
Don’t forget
to cool down and stretch!”

I just used my Mac’s speech synth, and parked my phone near to the
speaker on
record, in a quiet room (except for the planes every minute heading down
to
Heathrow). There’d be “bonus points” for actually creating the MP3
directly. Of
course, you don’t really need to get the computer to speak. It could
just print
out the messages at the appropriate time.

I thought that this problem was actually a lot more subtle than it
seemed on the outside. Getting the coach to put together sensible and
varied sentences seemed to be hard. I really wanted to find a much
more elegant solution, perhaps some sort of template based approach;
so far that has eluded me though. It seems to be something that
doesn’t easily factor down in to simple and clean functions. I had
trouble, at least.

Here’s the chunk of code that I have - it’s currently only for week
3! You could, of course, write a similar function for the other
weeks, but there’s got to be a better way than that?

def count_down(s, activity)

Encouragement for the last few seconds (which could get annoying

on longer runs?).
counts = { 10 => “10 more seconds.”,
20 => “20 seconds to go.”,
30 => “Half a minute to go.”,
60 => “You have 1 more minute of #{activity} left.”,
90 => “You have 1 and a half minutes of #{activity}
to go.”}

Add in encouragement / prompts for minutes.

[2, 3, 4, 6, 8, 10, 12, 15, 20, 25, 30].each {|m| counts[m*60] =
“You have #{m} minutes of #{activity} to go.”}

Build an ordered array of the possible lengths of time, and find

the index of this

activity’s length.

times = counts.keys.sort
start_index = times.index(s); raise “#{s} is not a known time.”
unless start_index

Count down through the time prompts. I bet inject could do this

too :slight_smile:
start_index.downto(0) do |i|
this_time = times[i]
next_time = i>0 ? times[i-1] : 0
delay_to_next = this_time - next_time
message = counts[this_time]
say message
wait delay_to_next
end
end

def say(to_say)
system(“say “#{to_say}””)
end

def wait(s)
@wait_until ||= Time.now
@wait_until += s
while((w = @wait_until - Time.new) > 0)
sleep w
end
end

For testing it’s really helpful to redefine the above to…

def say(m); puts m; end
def wait(s); puts “Waiting for #{s} seconds.”; end

Code to deal with just week 3!

def week_3
wait(0)
say “Start your first short run.”
count_down(90, ‘running’)
say “Stop running now. You have 1 long run and two short ones left.”
count_down(90, ‘walking’)

say “Start the first long run now.”
count_down(360, ‘running’)
say “Stop running now. You have a short run and a long run left.”
count_down(3
60, ‘walking’)

say “Start your second short run.”
count_down(90, ‘running’)
say “Stop running. You have 1 more long run left.”
count_down(90, ‘walking’)

say “Start your last run now.”
count_down(360, ‘running’)
say “Stop running. After this walk, you will have finished.”
count_down(3
60, ‘walking’)

say “Great! You’ve finished for today.”
end

Call week 3’s code.

week_3

Here’s my attempt:
At the moment it only prints text to the screen. I have an
interesting idea about generating a wav file directly, but I’m going
to have trouble getting that done before the summary deadline. The
script gets the exercise plan from a text file with a simple format.
It doesn’t do any error checking of arguments or file format.

Dude - that’s extremely cool :slight_smile: It’s a hell of a lot more comprehensive
than my attempt. Good work :slight_smile:

I really like the “encouragometer” member state, your use of
randomisation, and the way you break up the phase in to steps - it seems
a much less rigid solution than my own.

You have got it giving lots of propper phrases too, by just getting on
with the job and using lots of branching :slight_smile: I got far too hung up with
trying to unify it all, and didn’t really get anywhere at all! Perhaps
from the point you’ve got to now, it would be possible to factor out
some common ideas? Perhaps some kind of automatic pluralisation would be
useful in “summarize”? Perhaps it’s not worth it though?!

I was thinking that catagorisation of a phase in to “long” or “short”
could also go in to the definition file? That would cope with weeks
where there are no longer or shorter runs, and would easily allow
tailoring to each week’s requirements. It would be a phase description
to go along with the phase’s activity.

Maybe I’ll have to revisit mine again now :slight_smile:

Cheers,
Benjohn

Here’s my attempt:
At the moment it only prints text to the screen. I have an
interesting idea about generating a wav file directly, but I’m going
to have trouble getting that done before the summary deadline. The
script gets the exercise plan from a text file with a simple format.
It doesn’t do any error checking of arguments or file format.

Usage: ruby -d coach.rb weekly_plan.txt
Use the -d switch unless you want to wait 20 minutes for all the output.

-----week3.txt-----
run 90
walk 90
run 180
walk 180
run 90
walk 90
run 180
walk 180

-----coach.rb-----
$CheerThreshold = 6 #decrease to get more random encouragement
$LongThreshold = 120 #minimum time to be considered a “long” run

class Phase
attr_reader :action, :seconds
def initialize action, time
@action = action.downcase
@seconds = time.to_i
end
end

class Coach
def initialize filename
File.open(filename) {|f|
@rawdata = f.read.split("\n")
}
@duration = 0
@runs = @longs = @walks = 0
@encouragometer = 0
@step = [30,15,10,5,5]
end

def coach
build_timeline
say summarize(2)
say start_prompt
@time = Time.now
@target_time = @time
while (phase = @phases.shift)
update_summary phase
narrate_phase phase
if @phases.size > 0
say transition(@phases[0].action)
say summarize(rand(2))
end
end
say finish_line
end

def narrate_phase phase
say what_to_do_for(phase)
@target_time += phase.seconds
delta = (@target_time - Time.now).to_i
stepidx = 0
while (delta > 0)
stepidx+=1 if delta < @step[stepidx]+1
wait_time = delta%@step[stepidx]
wait_time += @step[stepidx] if wait_time <= 0
wait(wait_time)
delta = (@target_time - Time.now).to_i
encourage_maybe
say whats_left(phase.action,delta) if delta > 0
end
end

def update_summary phase
@duration -= phase.seconds
@runs -= 1 if phase.action == ‘run’
@longs -= 1 if phase.action == ‘run’ and phase.seconds >=
$LongThreshold
@walks -= 1 if phase.action == ‘walk’
end

def build_timeline
@phases = @rawdata.map {|command|
p = Phase.new(*command.split)
@duration += p.seconds
@runs += 1 if p.action == ‘run’
@longs += 1 if p.action == ‘run’ and p.seconds >= $LongThreshold
@walks += 1 if p.action == ‘walk’
p
}
end

def say s
puts s
#todo: replace with speech
end
def wait n
if $DEBUG
puts “…waiting #{n} seconds…”
@target_time -= n
else
$stdout.flush
sleep(n)
end
end

def encourage_maybe
@encouragometer += rand(3)
if (@encouragometer > $CheerThreshold)
say cheer
@encouragometer = 0
end
end

def timesay secs
secs = secs.to_i
s = “”
if secs > 60
min = secs/60
secs -= min*60
s += “#{min} minute”
s += ‘s’ if min > 1
s += ’ and ’ if secs > 0
end
if secs > 0
s += “#{secs} second”
s += ‘s’ if secs > 1
end
s
end

All the phrases should be below this line, not mixed up in the logic

def what_to_do_for phase
s = “#{phase.action} for #{timesay(phase.seconds)} \n”
s += “You are almost done” if @phases.size == 1
s
end
def whats_left act, time
timestr = timesay(time)
s = [
“You have #{timestr} more to #{act}”,
“#{act} for #{timestr} more”,
“only #{timestr} left of #{act}ing”,
“You have #{timestr} more to #{act}”,
“#{timestr} left in this phase”,
“There are #{timestr} until the next activity”
]
s[rand(s.size)]
end
def start_prompt
“are you ready, go!”
end
def transition next_act
s = [“OK, you can #{next_act} now”,
“get ready to #{next_act}”]
s[rand(s.size)]
end
def finish_line
“you are done, rest now.”
end
def cheer
c = [“Keep it up!”, “Way to go!”, “Good Job!”]
c[rand(c.size)]
end
def summarize degree
shorts = @runs - @longs
s = “you have #{timesay(@duration)}”
if degree > 0
if degree > 1
s+= " for "
else
s+= " to go and there are " if @runs > 0
end
s+="#{@longs} long run" if @longs > 0
s+=“s” if @longs > 1
s+=" and" if @longs > 0 and shorts > 0 and degree <=1
s+=" #{shorts} short run" if shorts > 0
s+=“s” if shorts > 1
if degree >1
s+=" and" if @longs+shorts > 0
s+=" #{@walks} walk" if @walks > 0
s+=“s” if @walks > 1
else
s+=" left"
end
else
s+= " left to exercise"
end
s
end
end

Coach.new(ARGV[0]||“week3.txt”).coach

-Adam

On 6/14/06, Adam S. [email protected] wrote:

Here’s my attempt:

Here’s a quick version that is closer to having speech synth. It’s not
a real synthesiser, but if you can provide the corresponding ogg files
it can look for certain phrases and play them. The result should sound
a bit better than a real synthesiser since the sections will be spoken
fairly naturally. Only 53 files to record!

Well maybe the strings you use could be made a bit more similar to
shorten the list a bit :wink:

Les


$CheerThreshold = 6 #decrease to get more random encouragement
$LongThreshold = 120 #minimum time to be considered a “long” run

class SpeechSynth
def initialize
@known = [“minutes left in this phase”,
“you are done, rest now.”,
“until the next activity”,
“OK, you can walk now”,
“seconds more to walk”,
“seconds more to run”,
“to go and there are”,
“OK, you can run now”,
“You are almost done”,
“are you ready, go!”,
“get ready to walk”,
“get ready to run”,
“short run left”,
“in this phase”,
“to exercise”,
“Keep it up!”,
“to exersize”,
“Way to go!”,
“of walking”,
“short runs”,
“minute and”,
“There are”,
“Good job!”,
“long runs”,
“of runing”,
“You have”,
“walk for”,
“long run”,
“minutes”,
“seconds”,
“to walk”,
“run for”,
“to run”,
“walks”,
“left”,
“only”,
“more”,
“for”,
“and”,
“60”,
“30”,
“15”,
“18”,
“55”,
“7”,
“6”,
“5”,
“9”,
“3”,
“2”,
“1”,
“8”,
“4”]
end

def playFile(fileName)
	puts "PLAY: '" +fileName + "'"
	#on linux this can be:
	#`play #{filename}`
end

def known(searchPhrase)
	@known.each do |phrase|
		return $~[1] if searchPhrase =~ /^(#{phrase}).*/i
	end
	puts "UNKNOWN PHRASE: #{searchPhrase}"
	return nil
end

def say(sentence)
	while sentence.length > 0
		knownPhrase = known(sentence)
		if knownPhrase
			playFile(knownPhrase + ".ogg")
			sentence = sentence[knownPhrase.length+1..-1]
			sentence = "" if !sentence
		else
			sentence = ""
		end
		sentence.strip!
	end
end

end

class Phase
attr_reader :action, :seconds
def initialize action, time
@action = action.downcase
@seconds = time.to_i
end
end

class Coach
def initialize filename
@synth = SpeechSynth.new
File.open(filename) {|f|
@rawdata = f.read.split(“\n”)
}
@duration = 0
@runs = @longs = @walks = 0
@encouragometer = 0
@step = [30,15,10,5,5]
end

def coach
build_timeline
say summarize(2)
say start_prompt
@time = Time.now
@target_time = @time
while (phase = @phases.shift)
update_summary phase
narrate_phase phase
if @phases.size > 0
say transition(@phases[0].action)
say summarize(rand(2))
end
end
say finish_line
end

def narrate_phase phase
say what_to_do_for(phase)
@target_time += phase.seconds
delta = (@target_time - Time.now).to_i
stepidx = 0
while (delta > 0)
stepidx+=1 if delta < @step[stepidx]+1
wait_time = delta%@step[stepidx]
wait_time += @step[stepidx] if wait_time <= 0
wait(wait_time)
delta = (@target_time - Time.now).to_i
encourage_maybe
say whats_left(phase.action,delta) if delta > 0
end
end

def update_summary phase
@duration -= phase.seconds
@runs -= 1 if phase.action == ‘run’
@longs -= 1 if phase.action == ‘run’ and phase.seconds >=
$LongThreshold
@walks -= 1 if phase.action == ‘walk’
end

def build_timeline
@phases = @rawdata.map {|command|
p = Phase.new(*command.split)
@duration += p.seconds
@runs += 1 if p.action == ‘run’
@longs += 1 if p.action == ‘run’ and p.seconds >= $LongThreshold
@walks += 1 if p.action == ‘walk’
p
}
end

def say s
@synth.say(s)
end

def wait n
if $DEBUG
puts “…waiting #{n} seconds…”
@target_time -= n
else
$stdout.flush
sleep(n)
end
end

def encourage_maybe
@encouragometer += rand(3)
if (@encouragometer > $CheerThreshold)
say cheer
@encouragometer = 0
end
end

def timesay secs
secs = secs.to_i
s = “”
if secs > 60
min = secs/60
secs -= min*60
s += “#{min} minute”
s += ‘s’ if min > 1
s += ’ and ’ if secs > 0
end
if secs > 0
s += “#{secs} second”
s += ‘s’ if secs > 1
end
s
end

All the phrases should be below this line, not mixed up in the logic

def what_to_do_for phase
s = “#{phase.action} for #{timesay(phase.seconds)} \n”
#s += “You are almost done” if @phases.size == 1
s
end
def whats_left act, time
timestr = timesay(time)
s = [
“You have #{timestr} more to #{act}”,
“#{act} for #{timestr} more”,
“only #{timestr} left of #{act}ing”,
“You have #{timestr} more to #{act}”,
“#{timestr} left in this phase”,
“There are #{timestr} until the next activity”
]
s[rand(s.size)]
end
def start_prompt
“are you ready, go!”
end
def transition next_act
s = [“OK, you can #{next_act} now”,
“get ready to #{next_act}”]
s[rand(s.size)]
end
def finish_line
“you are done, rest now.”
end
def cheer
c = [“Keep it up!”, “Way to go!”, “Good Job!”]
c[rand(c.size)]
end
def summarize degree
shorts = @runs - @longs
s = “you have #{timesay(@duration)}”
if degree > 0
if degree > 1
s+= " for "
else
s+= " to go and there are " if @runs > 0
end
s+=“#{@longs} long run” if @longs > 0
s+=“s” if @longs > 1
s+=" and" if @longs > 0 and shorts > 0 and degree <=1
s+=" #{shorts} short run" if shorts > 0
s+=“s” if shorts > 1
if degree >1
s+=" and" if @longs+shorts > 0
s+=" #{@walks} walk" if @walks > 0
s+=“s” if @walks > 1
else
s+=" left"
end
else
s+= " left to exercise"
end
s
end
end

Coach.new(ARGV[0]||“week3.txt”).coach

On 6/16/06, Adam S. [email protected] wrote:

but I wanted to share.

I had an idea similar to Leslie’s, but I wanted to actually write out
an audio file, instead of sending the narration to the speakers. The
solution has 2 parts.

What we need now is your master wav file! This software is cool, let’s
use it!
(look mummy, there goes a fit geek!)

Les

On 6/16/06, Leslie V. [email protected] wrote:

What we need now is your master wav file! This software is cool, let’s use it!
(look mummy, there goes a fit geek!)

File is at:
http://rubyurl.com/i6x

There are at least 3 problems with it

  • It’s missing a bunch of numbers, so you often get useless messages
    like: “you have . left to run” . Look for the “Can’t Find …”
    messages in the output to see what other numbers you need.
  • the pauses between words are too long.
  • The voice is really annoying.

If you are really interested, I’d record your own voice, or someone
who motivates you.
The process to mark it up is actually simple.
Make sure there is a bit of silence between each word in your recording.
Then use the AutoCue feature of your wave editor to insert cues at the
end of each silence. - I used GoldWave, which made it really easy.
Delete any false cues in the middle of words, and add any missing
ones. - I only found one extra cue.
Then run this script, making sure that the list of words matches what
you recorded.

-----cuefixer.rb-----
require ‘wavespeaker.rb’
Words = %w( run walk for second seconds minute minutes you are almost
done
have more to only left this phase there until the next activity
ready go ok can now get rest keep it up way good job and long short
runs walks exercise 1 2 3 4 5 6 10 15 30 60 in)

class CueFixer < RiffRead
def initialize io
super
raise “Not a Wave File” if @type != ‘WAVE’
@out = File.open(“recued_coach.wav”, “wb”)
@out.write(‘RIFF’)
@filesize_marker = @out.pos
@out.write [0].pack(‘V’)
@written = @out.write(‘WAVE’)
end
def close
@out.seek @filesize_marker
@out.write [@written].pack(‘V’)
@out.seek @listsize_marker
@out.write [@written-@liststart].pack(‘V’)
@out.close
p @written
end
def handle_tag tag,size
@written += @out.write tag
chunksize_marker = @out.pos
@written += @out.write [size].pack(‘V’)
funcname = “parse_”+tag.strip
if methods.include? funcname
size = self.send(funcname, size)
here = @out.pos
@out.seek chunksize_marker
@out.write [size].pack(‘V’)
@out.seek here
else
data = @io.read(size)
@written += @out.write data
data
end
end
def handle_list
@written += @out.write ‘LIST’
@listsize_marker = @io.pos
@liststart = @written+4
@written += @out.write @io.read(8)
end
def parse_labl size
id = get_long
osize = @out.write [id].pack(‘V’)
@written+=osize
string = @io.read(size-4)
newcue = Words[id]
p newcue
@written += @out.write(newcue)
@written += @out.write “\0”
osize += newcue.size + 1
if osize%2 != 0
@written += @out.write “\0”
end
osize
end
end

w = CueFixer.new(File.new(“fullcoach.wav”,“rb”))
w.parse
w.close
-----end-----

Good Luck,
-Adam

On 6/14/06, Leslie V. [email protected] wrote:

Here’s a quick version that is closer to having speech synth. It’s not
a real synthesiser, but if you can provide the corresponding ogg files
it can look for certain phrases and play them. The result should sound
a bit better than a real synthesiser since the sections will be spoken
fairly naturally. Only 53 files to record!

I know this part is after the summary (Thanks for the nice writeup) ,
but I wanted to share.

I had an idea similar to Leslie’s, but I wanted to actually write out
an audio file, instead of sending the narration to the speakers. The
solution has 2 parts.

class WaveRead extracts all the information from a wave file. I put
it together in under 2 hours last night. It was so much easier to
write than the one in I did C a few years ago, and I’m really pleased
the result. It’s clean and extensible. I already have an idea for
making it trivial to add the other chunk definitions.

class WaveSpeaker writes a new wave file with everything it was told
to say. It does this by using a wave file feature called cues, which
are a way of marking a point in the file and giving it a name. I
created a wave file with several words, and a cue marking each one.
(see more about this below.) WaveSpeaker parses this file, and starts
writing a new output file with the same format. Then, when #say is
called, it looks for each word in the list of cues, and if found,
pastes the appropriate part of the source wave into the output file.
It inserts silence for each #wait, compensating for the length of the
previous sentences. At the end it just fixes up the filesize data,
and closes the file. All you need to do is convert the file to MP3
and transfer to your iPod.

------wavespeaker.rb------
require ‘Ostruct’

class RiffRead
def initialize io
@io = io
raise “Not a RIFF file” if io.read(4) != “RIFF”
@size = get_long
@type = get_word
end
def parse
chunks = []
chk = get_chunk
while chk
chunks << chk
chk = get_chunk
end
chunks
end
def self.get_long io
io.read(4).unpack(‘V’)[0]
end
def self.get_short io
io.read(2).unpack(‘v’)[0]
end
def self.get_word io
io.read(4)
end

private
def get_chunk
tag = get_word
return nil if !tag
if tag == ‘LIST’
handle_list
else
size = get_long
size+=1 if size%2 != 0
data = handle_tag(tag,size)
data ||= @io.read(size)
[tag, size, data]
end
end
def handle_tag tag,size
funcname = “parse_”+tag.strip
if methods.include? funcname
return self.send(funcname, size)
end
end
def handle_list
listsize = get_long
@listtype = get_word
[‘LIST’,listsize,@listtype]
end
def get_long
self.class::get_long @io
end
def get_short
self.class::get_short @io
end
def get_word
self.class::get_word @io
end
end

def make_cue io
cue = OpenStruct.new
cue.name = RiffRead::get_long io
cue.position = RiffRead::get_long io
cue.chkname = RiffRead::get_word io
cue.chkstart = RiffRead::get_long io
cue.blockkstart = RiffRead::get_long io
cue.samplestart = RiffRead::get_long io
cue
end

class WaveRead < RiffRead
attr_reader :cues,:labels,:format, :data
def initialize io
super
raise “Not a Wave File” if @type != ‘WAVE’
end
def parse_fmt size
@format = OpenStruct.new
@format.data = @io.read(size)
@format.size = size
@format.tag = format.data[0,2].unpack(‘v’)[0]
@format.channels = format.data[2,2].unpack(‘v’)[0]
@format.samples_per_sec = format.data[4,4].unpack(‘V’)[0]
@format.bytes_per_sec = format.data[8,4].unpack(‘V’)[0]
@format.blockAlign = format.data[12,2].unpack(‘v’)[0]
@format
end
def parse_data size
@data = @io.read(size)
end
def parse_cue size
@cues = []
numcues = get_long
numcues.times do
@cues << make_cue(@io)
end
@cues
end
def parse_labl size
id = get_long
string = @io.read(size-4)
@labels||=[]
@labels << [id,string.strip]
@labels.last
end
def parse_note size
id = get_long
string = @io.read(size-4)
@notes||=[]
@notes << [id,string.strip]
@notes.last
end
end

class WaveSpeaker
def initialize filename
File.open(filename, “rb”) do |f|
@data = WaveRead.new(f)
@data.parse
end
@elapsed = 0
end
def begin outfile
@out = File.open(outfile, “wb”)
@out.write(‘RIFF’)
@filesize_marker = @out.pos
@out.write [0].pack(‘V’)
@written = @out.write('WAVEfmt ')
@written+= @out.write [@data.format.size].pack(‘V’)
@written+= @out.write @data.format.data
@written+= @out.write(‘data’)
@datasize_marker = @out.pos
@written+= @out.write [0].pack(‘V’)
end
def say string
fixup(string).split.each do |str|
str = fixup(str)
if str == ‘COMMA’
wait 0.2
else
cue_id = nil
@data.labels.each_with_index{|label,i|
if label[1].downcase == str.downcase
cue_id = i
break
end
}
if cue_id
#p “saying #{str}”
start = @data.cues[cue_id].samplestart2
endpt = @data.cues[cue_id+1].samplestart
2
endpt+=1 if (endpt-start)%2 != 0
@written+= @out.write(@data.data[start…endpt])
@elapsed += (endpt-start).to_f / @data.format.bytes_per_sec
else
p “CAN’T FIND <#{str}>”
end
end
end
end
def wait seconds
a = “\0”
delay = (seconds - @elapsed)
p delay
if delay > 0
bytes = (delay * @data.format.bytes_per_sec).to_i
p “wait #{bytes}”
bytes+=1 if (bytes%2 != 0)
silence = a*bytes
@written+= @out.write silence
@elapsed = 0
else
@elapsed -= seconds
end
end
def fixup str
#remove punctuation, mark pauses
str.gsub!(/,/," COMMA “)
str.gsub!(/[^\w\s]/,”")
str
end
def quit
@out.seek @filesize_marker
@out.write [@written].pack(‘V’)
@out.seek @datasize_marker
@out.write [@written-@datasize_marker+4].pack(‘V’)
@out.close
p @written
end
end

if FILE == $0
wr = WaveSpeaker.new(“coach.wav”)
wr.begin(“todays_run.wav”)
wr.say ‘run 60 seconds’
wr.wait 1
wr.say ‘walk 15 minutes’
wr.quit
end
-----end-----

To get to work with my solution, just add the following lines:
in Coach#initialize, add
@speaker = WaveSpeaker.new “coach.wav”
@speaker.begin “current_workout.wav”

at the end of Coach#coach add
@speaker.quit

and replace these two functions:
def say s
@speaker.say s
end
def wait n
@speaker.wait n
@target_time -= n
end

To get the source file, I generated a wave file with 53 words from my
coaching script using a synth (couldn’t find a microphone), and used
my wave editor’s auto cue feature to insert numbered cues in all the
gaps between words. After running simple script to replace the
numbers with the words, I have a complete solution that produces a 20
minute long wav file of a robot coach. It would probably be better if
you used a real voice. If anyone is actually interested in this, I
can give you more details on the wave file creation.

-Adam