# Running Coach (#82)

**URL:** <https://www.ruby-forum.com/t/running-coach-82/62922>\
**Category:** Ruby\
**Created:** [June 9, 2006, 3:30pm UTC](https://www.ruby-forum.com/t/running-coach-82/62922 "2006-06-09T15:30:39Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 9, 2006, 3:30pm UTC](https://www.ruby-forum.com/t/running-coach-82/62922/1 "2006-06-09T15:30:39Z")

</div>

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/](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.

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 13, 2006, 8:27am UTC](https://www.ruby-forum.com/t/running-coach-82/62922/2 "2006-06-13T08:27:40Z")

</div>

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 🙂  
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(3_60, ‘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(3_60, ‘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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 14, 2006, 10:56am UTC](https://www.ruby-forum.com/t/running-coach-82/62922/3 "2006-06-14T10:56:12Z")

</div>

> 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 🙂 It’s a hell of a lot more comprehensive  
than my attempt. Good work 🙂

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 🙂 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 🙂

Cheers,  
Benjohn

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 14, 2006, 3:44am UTC](https://www.ruby-forum.com/t/running-coach-82/62922/4 "2006-06-14T03:44:33Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 14, 2006, 4:08pm UTC](https://www.ruby-forum.com/t/running-coach-82/62922/5 "2006-06-14T16:08:20Z")

</div>

On 6/14/06, Adam S. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) 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 😉

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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 16, 2006, 4:24pm UTC](https://www.ruby-forum.com/t/running-coach-82/62922/6 "2006-06-16T16:24:43Z")

</div>

On 6/16/06, Adam S. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) 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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 17, 2006, 7:55am UTC](https://www.ruby-forum.com/t/running-coach-82/62922/7 "2006-06-17T07:55:54Z")

</div>

On 6/16/06, Leslie V. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) 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](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

---

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 16, 2006, 1:49am UTC](https://www.ruby-forum.com/t/running-coach-82/62922/8 "2006-06-16T01:49:54Z")

</div>

On 6/14/06, Leslie V. [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) 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].samplestart_2  
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
