Quick Voice Prompts

by PopCultureSoft March 2012

There are a couple of iOS apps that I've developed that use voice prompts. One of these apps is a reading trainer for dolch words. Dolch words are a few hundred words that readers should be able to read by sight. I wrote the app for my son. It displays a word, which he attempts to read. When he's done, he presses a button to hear the word, and then presses a thumbs up or thumbs down button to score himself. Simple little app.

But I didn't really want to record a few hundred words to individual files, especially with my other children running around making noise in the background. So, I had my mac do it for me automatically. You probably know that the Mac has speaking capabilities. Using those capabilities and AppleScript, the Mac can record synthesized voice to an audio file. The advantages of this are very fast audio file production, no background noise, and cost effectiveness. So, it is very good for development purposes.

Launch AppleScript Editor from your Utilities menu. Type in the following command

say "hello"

Then click the Run button. The mac will literally say "hello". Very simple. Now we have it save that to a file. Replace "username" with your local username.

say "hello" saving to "/users/username/desktop/hello.aif"

This will create an audio file on your desktop that is a recording of the mac os synthesized voice saying "hello".

Now, to create hundreds of these files, I made a spreadsheet. I use OpenOffice, but you could do it in Excel, or Numbers. In your A column, type the words or phrases you want. I was doing single words.

always
around
because
been
before
...

In the second column, I typed a formula to generate the AppleScript command:

="say """&A1&""" saving to ""/users/username/desktop/"&A1&".aif"""

Then I copied this down to the rest of the words. The "fill down" command (ctrl-d in excel) makes quick work of this. Voila, 220 lines of applescript code!

say "always" saving to "/users/username/desktop/always.aif"
say "around" saving to "/users/username/desktop/around.aif"
say "because" saving to "/users/username/desktop/because.aif"
say "been" saving to "/users/username/desktop/been.aif"
say "before" saving to "/users/username/desktop/before.aif"
...

Run that and multiple files will appear on your desktop, or wherever you told it to in the path. Then you can convert them to caf files for the iOS using afconvert.

for f in *; do if [ "$f" != "converttocaf.sh" ] then /usr/bin/afconvert -f caff -d LEI16 $f echo "$f converted" fi done

Tags:

Coding

Taming the UIButton

by PopCultureSoft November 2011

I keep wanting to do things with UIButton that it just can't do. I don't know how many times I've wanted to do something as simple as changing the color of the button, or the highlight color. At least in iOS 5 you can now change the highlight color, which is cool, but I want it to do more.

The number one work around with UIButton is to make graphics for each state of the button: Default, selected, highlighted, etc. The main problem I have with that is that the graphics are set sizes. I have to create new graphics for each button size. This is very annoying, especially if I want to theme all my buttons in an app a particular color. Sometimes, I just want the button to be red, to give the user a visual queue that this button will do something important, or nasty, like deleting an entry. iOS uses a red button when deleting a line in a UITableView, so why can't I?

Another way that you can change the look of the button is to make changes to the layer of the button. I placed a button in interface builder and wired it up. Then I modified it this way:

[[basicButton layer] setCornerRadius:18.0f];
[basicButton setBackgroundColor:[UIColor whiteColor]];
[[basicButton layer] setBorderWidth:0.25f];
[[basicButton layer] setBorderColor:[[UIColor greenColor] CGColor]];

This works, somewhat. You will get a button of a different color. But it won't change color when you tap it. It also shows an anomaly in the corners when it's drawn. The outside of the border is not clean, for some reason

Also, if you set the border thickness very thin, some of the border is removed by the clipping mask of the layer. This may be the reason the above image isn't clean. Setting the buttons "clipsToBounds" property to NO does not fix this problem. That setting has to do with subviews, and this isn't a subview, but the layer. I tried setting the layers masksToBounds property to NO with no success.

 

 

To have total control of the button, you have to subclass it. So I bit the bullet, sub-classed UIButton, and forced it to do my will (insert evil scientist laugh here). It took me some time, but now I have it whenever I need to use it. At first I called it MyUIButton, but then changed it to ColorUIButton. I know you're asking yourself, "how does he come up with these creative names?" Since I have taken my share of help from other programming bloggers, It's my turn to give back. This isn't a very complicated subclass, and I'm certainly not the greatest coder in the world. So, please feel free to rip it apart, improve it.

In creating ColorUIButton, I wanted to be able to control certain characteristics of the button. This list grew as I found out that I could control many things. All of these items became properties of ColorUIButton:

• Button Color
• Button Highlight Color
• Border Color
• Border Thickness
• Border Highlight Color
• Corner Radius
• Gradient

The key to controlling all these features was overriding the drawRect method of UIButton. In doing so, I could use Core Graphics (Quartz 2D) to programatically draw a button based on those properties. The problem with the Layer

I've been doing electronic prepress work for 20 years. I used to be an Adobe Illustrator guru. I taught myself PostScript. So, I'm not sure if this is as easily understood by most coders or not. A good starting point is the Drawing and Printing Guide for iOS in the iOS Developer Library.

The drawRect method is an instance method of the UIView class. UIButton inherits from that class. You can draw anything you have a fancy to draw by overriding this method.

-(void)drawRect:(CGRect)rect {
     // insert drawing code here
}

The first item of business is to establish a CGContextRef:

CGContextRef myContext = UIGraphicsGetCurrentContext();

This sets up a Quartz 2D environment in which to draw, based on the current context, which UIView establishes. (If you're not in a UIView, you need to push your own context, but that's another story.)

Next we need to create a CGPath. You could just draw straight to the context, but I need a path that I can reuse. If I draw straight to the context, the path will be flushed when I "paint" it. If it weren't for the rounded corners, I could create a rectangle with one funciton: CGPathAddRect. But, alas, we need to actually move around and draw a precise shape. A normal rectangle has 4 points, and you just need to connect those points with lines. A round corner rectangle has 8 points, 2 for each corner. The 2 corner points are connected by an arc. the distance from the bounding rectangle to each point is equal to the radius of the arc.

1) Create a new path

CGMutablePathRef myPath = CGPathCreateMutable();

2) Move to the first point. In a normal rectangle, this would be (0,0). In a rounded rectangle, this is (radius,0)

CGPathMoveToPoint(myPath, NULL, radius, 0);

3) Add a line to the next point (width - radius, 0);

CGPathAddLineToPoint(myPath, NULL, width - radius, 0);

4) Add an arc to the next point. The arc function requires the center point of the arc (width - radius , radius), the radius, the start and end angles, and a boolean indicating the direction is clockwise. We are moving counter clockwise, so this is 0.

CGPathAddArc(myPath, NULL, width - radius, radius, radius, radians(-90), 0, 0);

5) Now we just keep going around the path doing the same thing.

CGPathAddLineToPoint(myPath, NULL, width, height - radius);
CGPathAddArc(myPath, NULL, width - radius, height - radius, radius, radians(0), radians(90), 0);
CGPathAddLineToPoint(myPath, NULL, radius, height);    
CGPathAddArc(myPath, NULL, radius, height - radius, radius, radians(90), radians(180), 0);
CGPathAddLineToPoint(myPath, NULL, 0, radius);
CGPathAddArc(myPath, NULL, radius, radius, radius, radians(180), radians(270), 0);

6) Then we add this path to the context and fill it

CGContextAddPath(myContext, myPath);
CGContextSetFillColor(myContext, cgFillColor);
CGContextDrawPath(myContext, kCGPathFill);

Now we can add it again to stroke it

CGContextAddPath(myContext, myPath);    
CGContextSetStrokeColor(myContext, cgStrokeColor);    
CGContextSetLineWidth(myContext, self.lineWidth);
CGContextDrawPath(myContext, kCGPathStroke);

You could do both fill and stroke at the same time, but I needed to add a gradient to it in between. The gradient is there to add a highlight to the button over the fill, but not over the stroke. Gradients in Core Graphics will fill the entire area of the UIView, and so need a clipping path to constrain them.

CGContextAddPath(myContext, myPath);        
CGContextClip(myContext);
CGPoint myStartPoint, myEndPoint;
myStartPoint.x = x;
myStartPoint.y = height - y;
myEndPoint.x = x;
myEndPoint.y = y;
CGContextDrawLinearGradient (myContext, myGradient, myStartPoint, myEndPoint, 0);
CGGradientRelease(myGradient);
CGColorSpaceRelease(myColorspace);

You can download the source code to see the entire drawRect method. I also set the colors based on a BOOL variable called "tap". If the user is tapping on the button, the colors change accordingly. To do this I had to override other methods in the code:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    tap = YES;
    [self setNeedsDisplay];
    [super touchesBegan:touches withEvent:event];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    tap = NO;
    [self setNeedsDisplay];
    [super touchesEnded:touches withEvent:event];
}

The "[self setNeedsDisplay]" tells the view that it needs to draw itself. Otherwise, nothing will change. It isn't an animation, so you have to explicitly tell it when to draw itself. We have to pass on the touch event to the superclass so that the object actually behaves as a UIButton.

Then I also had to override another class to handle the behaviour of UIButton when people tap, then drag their finger outside the view

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    float distance = 70.0;
    CGPoint touchPoint = [[touches anyObject] locationInView:self];
    CGRect testRect = CGRectMake(-distance, -distance, self.frame.size.width + distance * 2, self.frame.size.height + distance * 2);
    if (CGRectContainsPoint(testRect, touchPoint)) {
        tap = YES;
        [self setNeedsDisplay];
    }
    else {
        tap = NO;
        [self setNeedsDisplay];
    }
    [super touchesMoved:touches withEvent:event];
}

The distance of 70 was purely trial and error. When a user taps and drags their finger outside of a UIButton, it will stay active until the touch event is greater than 70 points away from the button edge. So, I made a CGRect that was 70 points away from the bounds of the button, and the CGRectContainsPoint function tells me if it is still in that rect.

Here is the end effect. You can add UIButtons to your nib file, but change the class of them to ColorUIButton. Wire them up in the view controller and set the parameters you need.

 Now, as I said before, iOS 5 did give us the ability to change the highlight color. Perhaps in the future they will also let us change the button color, corner radius, etc. But until then, I have this custom class that I can use.There are other settings that are explained in the source code. For example, I had to inset the button based on the width of the border, or the border would extend beyond the boundries of the button, which we don't want.Here is the source code: button tests.zip (93.40 kb)

 In my source code the nib file looks like this:

When it runs, it looks like this:

Tags: , ,

Coding

Keep up-to-date - Join @PopCultureSoft on Twitter or Facebook for the latest news...

Pop Culture Software is an iOS development company located in Cortlandt Manor, NY.