When discussing digital data and audio files it is important to distinguish the
STORAGE MEDIA from the STORAGE FORMAT from the TRANSMISSION FORMAT. STORAGE MEDIA
for DIGITAL audio can be a hard drive, a SD memory card, an audio CD, internal
computer memory, and more. STORAGE MEDIA for ANALOG audio include phonograph
records and audio tape. Likewise, the TRANSMISSION FORMAT for audio can be either
analog or digital. A simple earphone jack (like the one on your iPod) is ANALOG.
Your HD TV may be connected to satellite speakers by a DIGITAL HDMI cable.
Chirp controlled treadmills require ANALOG audio. This means the digitally stored
audio on your computer or iPod must be converted to analog. No problem since computers
and iPods are made to do this. Just hook up the earphone jack to the treadmill with a
suitable cable and you have the needed ANALOG connection in place.
It's interesting to note that the ANALOG audio chirp that goes to the treadmill actually
contains DIGITAL information. That's right. A binary command is modulated into audio
and sent to the treadmill where it is demodulated and executed.
In my iPhone app, the user creates a sequence of workout segments. Each segment consists
of a speed, incline, and duration. I build short audio WAV files, each of which "command"
the treadmill to the desired speed and incline. I "play" this WAV audio - then set a
timer for the segment duration. When the timer expires I "play" the audio "command" for
the next segment.
WAV audio is a relatively simple uncompressed DIGITAL audio format. I believe it was
defined by Microsoft in the early 1990s. To keep my uncompressed digital audio files
fairly small I create the files using monophonic audio and modest quality values of 8
bit samples with a 22.05 kHz sampling rate.
Treadmills controlled by SD cards are fundamentally very different. While the SD card
workout may contain audio, that audio is NOT used in controlling the treadmill. That
audio only exists to provide information, encouragement, and ...perhaps... some marketing.
Actual control of the treadmill speed and incline is accomplished by a sequence of
[very strange] binary commands.
Compatible treadmills only recognize specific directories and files as legal workouts. The minimum SD Card structure for a legal workout is:
iFit Tread layout.fit S000000A.fit W000000A.fitWhere:
This file specifies workouts available to the treadmill on this SD card. These workouts can
be accessed sequentially using the up/down arrows on the treadmill console. The zip file
emailed by my Treadmill Controller App includes a layout.fit file that always specifies the
same workout and sound files.
The analytic individual may want to use a BINARY editor (there are hidden, non-printable
characters in the file) to manually edit this file to specify multiple workouts instead of just
one. For example, one might change this file from:
W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* ********,********,********,* ********,********,********,* ********,********,********,*To:
W000000A,S000000A,********,* W000000B,S000000A,********,* W000000C,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* W000000A,S000000A,********,* ********,********,********,* ********,********,********,* ********,********,********,*Then, instead of renaming the emailed workout to W000000A.fit, your would rename one to W000000B.fit, another to W000000C.fit, and so on. Since the Treadmill Controller App does not include sound in the workouts, S000000A.fit is the only sound definition file needed or wanted.
In the file structure defined by the treadmill folks, 'S' files are the text files that specify the sound files played during a workout. Since the Treadmill Controller App uses no sound, this file is only an empty place holder. This same empty place holder can be used for every workout specified in the layout.fit file.
This is a binary file that tells the treadmill what to do. Each workout must have its own, distinct, file and treadmills like these files to be named W000000A.fit, W000000B.fit, W000000C.fit, ...
I stopped investigating the treadmill file structure when I learned enough to do what I wanted. Some of you out there may go far beyond me. If you learn anything cool, or have a comment, drop me an email. Be aware that whatever you share with me will be considered shared freely and I am under no responsibility to compensate you in any way.
Several years ago companies started making treadmills that could be controlled
from an external source. In the early days control was accomplished by applying
a series of audio "chirps" to a special input jack. More recently, treadmills
were manufactured that could be commanded by the insertion of a specially
prepared Secure Digital (SD) memory card. Files on the SD Card provided a
complete predetermined workout.
Unfortunately, many of these "factory made" workouts, both chirp and SD Card,
were costly and more like a media production than a workout. Version 4.0 of my
programs Treadmill Controller and Treadmill Controller Free allow the user to
economically take personal control of their treadmill exercise for both Chirp
and SD Card treadmills. These Apps use the professionally produced and tested
iOS Frameworks that are offered here. Using these Frameworks, iPhone developers
may incorporate treadmill control in any Application.
Treadmills are managed very differently by the Chirp and SD Card technologies.
The file(s) that constitute a workout must be created and written to an SD Card
in an appropriate directory structure. The controlling file has a very
particular format with checksums and anticipates a general structure and timing
to workouts. Chirps, however, are externally generated and can be applied to
the treadmill at any time.
Sample programs are provided that show experienced iPhone developers exactly
how to use these Frameworks.
This Framework is used to develop iPhone/iPad/iTouch Applications that can operate compatible treadmills with audio "Chirps".
This Framework is used to develop iPhone/iPad/iTouch Applications that can operate
compatible treadmills by inserting a specially prepared SD Card.
View or Download a PDF of the Chirp Framework API document.
View or Download a PDF of the SD Framework API document
Sample code to call Chirp and SD Card Framework APIs.
/* * chirpAPI.h * Treadmill Controller App * * Created by Mike Ficco on 12/29/10. * Copyright 2010-2015 Mike Ficco. All rights reserved. * * This file defines the EXTERNAL interface to the chirp iOS Framework */ /* Note that to play the audio (with AudioServicesPlaySystemSound and such) we need to: - include/import <AudioToolbox/AudioToolbox.h> - and add the AudioToolbox.framework. Do this by > right click on Frameworks > Add > Existing Frameworks > AudioToolbox.framework */ #import <AudioToolbox/AudioToolbox.h> #define ChirpERR_NONE 0x00000000 // No error detected #define ChirpERR_LIBOPEN 0x80000001 // The Chirp Library is already open #define ChirpERR_LIBCLOSED 0x80000002 // The Chirp Library has not yet been opened or is already closed #define ChirpERR_MALLOCFAILURE 0x80000003 // Problem allocating memory #define ChirpERR_BADSPEED 0x80000004 // The specified speed value is out of range #define ChirpERR_BADINCLINE 0x80000005 // The specified incline value is out of range #define MAX_SPEED 12.0 // Treadmill speed must be less than (or equal) 12 MPH #define MAX_INCLINE 12.0 // Treadmill incline must be less than (or equal) 12 degrees uint32_t getChirpLibraryVersion(void); uint32_t openChirpLibrary(void); uint32_t closeChirpLibrary(void); uint32_t makeChirp(float speed, float incline, uint32_t* bufSize, uint8_t** chirpBuffer); uint32_t makeStopChirp(uint32_t* bufSize, uint8_t** chirpBuffer);
/* * sdAPI.h * Treadmill Controller App * * Created by Mike Ficco on 01/10/11. * Copyright 2011-2015 Mike Ficco. All rights reserved. * * This file defines the EXTERNAL interface to the SD iOS Framework */ #define SdERR_NONE 0x00000000 // No error detected #define SdERR_LIBOPEN 0x80000001 // The SD Library is already open #define SdERR_LIBCLOSED 0x80000002 // The SD Library has not yet been opened or is already closed #define SdERR_MALLOCFAILURE 0x80000003 // Problem allocating memory #define SdERR_BADSPEED 0x80000004 // The specified speed value is out of range #define SdERR_BADINCLINE 0x80000005 // The specified incline value is out of range #define SdERR_BADTIME 0x80000006 // The specified time is illegal, not specified, or already used #define SdERR_BADINDEX 0x80000007 // The specified index could not be found #define SdERR_BADPOINTER 0x80000008 // The specified memory pointer is invalid #define SdERR_BADSTARTTIME 0x80000009 // The workout does not start at 0 seconds #define SdERR_WORKOUTINVALID 0x8000000A // No workout has been started #define SdERR_WORKOUTACTIVE 0x8000000B // A workout is already in progress #define SdERR_WORKOUTEMPTY 0x8000000C // A workout has been started, but there are no segments in it #define SdERR_WORKOUTTOOBIG 0x8000000D // The number of workout segments has gotten too large #define MAX_TIME 5400 // This is 90 minutes in seconds #define MAX_SPEED 12.0 // Treadmill speed must be less than (or equal) 12 MPH #define MAX_INCLINE 12.0 // Treadmill incline must be less than (or equal) 12 degrees typedef struct { uint16_t time; float speed; float incline; } WoSegment; uint32_t getSdLibraryVersion(void); uint32_t openSdLibrary(void); uint32_t closeSdLibrary(void); uint32_t startSdWorkout(void); uint32_t addSegmentToSdWorkout(uint16_t time, float speed, float incline); uint32_t finishSdWorkout(uint32_t* fileSize, uint8_t** fileBuf); uint32_t countSegmentsInSdWorkout(int* count); uint32_t listSegmentsInSdWorkout(int maxSegments, int* count, WoSegment* segmentBuf); uint32_t deleteSegmentByIndexFromSdWorkout(int segmentIndex); uint32_t deleteSegmentByTimeFromSdWorkout(uint16_t time);
/* * sample.h * Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks * * Created by Mike Ficco on 2011 05-29. * Copyright 2011-2015 Mike Ficco. All rights reserved. * * This file contains information used in the implementation of an example Treadmill Controller App. * It is independent of specific chirp or SD functions * * Version 1.00 * */ #import "ChirpAPI.h" #import "sdAPI.h" #define ENDSAMP 0xFFFF #define SEGMENTS_IN_SAMPLE 9 typedef struct { uint32_t duration; float speed; float incline; SystemSoundID soundID; } WOSEGMENT; extern void copySampleWorkout(WOSEGMENT* sampleWorkout);
/* * sample.m * Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks * * Created by Mike Ficco on 2011 05-29. * Copyright 2011-2015 Mike Ficco. All rights reserved. * * This file contains information used in the implementation of an example Treadmill Controller App. * It is independent of specific chirp or SD functions * * Version 1.00 * */ #import "sample.h" WOSEGMENT runThis[SEGMENTS_IN_SAMPLE] = { { 60, 3.0, 3.0 }, // Segment [ 0] { 60, 4.0, 4.0 }, // Segment [ 1] { 60, 4.0, 5.0 }, // Segment [ 2] { 60, 4.5, 5.0 }, // Segment [ 3] { 60, 5.0, 5.0 }, // Segment [ 4] { 60, 4.0, 5.0 }, // Segment [ 5] { 60, 4.0, 4.0 }, // Segment [ 6] { 60, 3.0, 3.0 }, // Segment [ 7] { ENDSAMP, 0.0, 0.0 } }; void copySampleWorkout(WOSEGMENT* sampleWorkout) { memcpy(sampleWorkout, runThis, sizeof(runThis)); }
// // ReferenceAppAppDelegate.h // ReferenceApp // // Created by Mike Ficco on 5/29/11. // Copyright Mike Ficco 2011-2015. All rights reserved. // #import <UIKit/UIKit.h> @class ReferenceAppViewController; @interface ReferenceAppAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window; ReferenceAppViewController *viewController; } @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet ReferenceAppViewController *viewController; @end
// // ReferenceAppViewController.h // ReferenceApp // // Created by Mike Ficco on 5/29/11. // Copyright Mike Ficco 2011-2015. All rights reserved. // #import <UIKit/UIKit.h> @interface ReferenceAppViewController : UIViewController { NSInteger nowPlaying; NSTimer* chirpTimer; IBOutlet UITextField* libVersion; IBOutlet UITextField* information; } @property (nonatomic) NSInteger nowPlaying; @property (nonatomic, retain) NSTimer* chirpTimer; @property (nonatomic, retain) IBOutlet UITextField* libVersion; @property (nonatomic, retain) IBOutlet UITextField* information; - (IBAction) makeChirpWorkout: (id) sender; - (IBAction) makeSdWorkout: (id) sender; @end
// // ReferenceAppAppDelegate.m // ReferenceApp // // Created by Mike Ficco on 5/29/11. // Copyright Mike Ficco 2011-2015. All rights reserved. // #import "ReferenceAppAppDelegate.h" #import "ReferenceAppViewController.h" @implementation ReferenceAppAppDelegate @synthesize window; @synthesize viewController; - (void)applicationDidFinishLaunching:(UIApplication *)application { // Override point for customization after app launch [window addSubview:viewController.view]; [window makeKeyAndVisible]; } - (void)dealloc { [viewController release]; [window release]; [super dealloc]; } @end
/* * ReferenceAppViewController.m * Reference implementation for using the Chirp and SD treadmill controller iOS Frameworks * * Created by Mike Ficco on 2011 05-29. * Copyright 2011-2015 Mike Ficco. All rights reserved. * * This file contains information used in the implementation of an example Treadmill Controller App. * * Version 1.00 * */ #import "sample.h" #import "ReferenceAppViewController.h" @implementation ReferenceAppViewController @synthesize nowPlaying; @synthesize chirpTimer; @synthesize libVersion; @synthesize information; /* // The designated initializer. Override to perform setup that is required before the view is loaded. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { // Custom initialization } return self; } */ /* // Implement loadView to create a view hierarchy programmatically, without using a nib. - (void)loadView { } */ /* // Implement viewDidLoad to do additional setup after loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; } */ /* // Override to allow orientations other than the default portrait orientation. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // Return YES for supported orientations return (interfaceOrientation == UIInterfaceOrientationPortrait); } */ - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } /* Treadmill Sample Code Follows */ WOSEGMENT sampleWorkout[SEGMENTS_IN_SAMPLE]; - (void) playChirp: (NSTimer*) timer { /* Because of the way the workout is built for this sample app, the last workout segment is the stop chirp, but the duration is still ENDSAMP */ self.nowPlaying++; if ( self.nowPlaying < SEGMENTS_IN_SAMPLE ) { AudioServicesPlaySystemSound(sampleWorkout[self.nowPlaying].soundID); if ( (self.nowPlaying+1) < SEGMENTS_IN_SAMPLE ) { /* This was NOT the stop chirp. The duration is legal and more chirps remain to be played for this workout */ self.chirpTimer = [NSTimer scheduledTimerWithTimeInterval: sampleWorkout[self.nowPlaying].duration target: self selector: @selector (playChirp:) userInfo: nil repeats: NO]; [self.information setText: [NSString stringWithFormat: @"Just played chirp %2d of %2d. Delaying %d sec", self.nowPlaying+1, SEGMENTS_IN_SAMPLE, sampleWorkout[self.nowPlaying].duration]]; } else { // We just played the last chirp self.chirpTimer = nil; [self.information setText: [NSString stringWithFormat: @"Finished playing all %2d chirps", SEGMENTS_IN_SAMPLE]]; } } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Internal Error" message: @"We have a very confused timer" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } - (IBAction) makeChirpWorkout: (id) sender { int loop; uint32_t result; uint32_t bufSize; CFURLRef mySoundURL; CFStringRef fileCFStringRef; FILE* chirpFile; uint8_t* chirpBuf; NSString* pathNSString; NSString* fileNSString; NSString* cacheDirectory; // Grab access to the cacheDirectory (where we store the chirp files) NSArray* filePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); cacheDirectory = [filePaths objectAtIndex: 0]; copySampleWorkout(sampleWorkout); // Get our predefined sample workout result = openChirpLibrary(); // ...and start using it if ( result == ChirpERR_NONE ) { [self.libVersion setBorderStyle: UITextBorderStyleNone]; [self.libVersion setBackgroundColor: [UIColor yellowColor]]; [self.libVersion setTextAlignment: UITextAlignmentCenter]; [self.libVersion setText: [NSString stringWithFormat:@"Using Chirp Library Version %8X", getChirpLibraryVersion()]]; for ( loop = 0; loop < SEGMENTS_IN_SAMPLE; loop++ ) { // Build any needed audio chirp files for this workout fileNSString = [NSString stringWithFormat:@"segment%2d.wav", loop]; pathNSString = [cacheDirectory stringByAppendingPathComponent: fileNSString]; fileCFStringRef = (CFStringRef) pathNSString; // Create the file URL for the chirp audio WAV file mySoundURL = CFURLCreateWithFileSystemPath(NULL, fileCFStringRef, kCFURLPOSIXPathStyle, false); if ( mySoundURL ) { // Create the controlling chirp files if ( sampleWorkout[loop].duration == ENDSAMP ) { // This is the last segment. Stop the treadmill makeStopChirp(&bufSize, &chirpBuf); } else makeChirp(sampleWorkout[loop].speed, sampleWorkout[loop].incline, &bufSize, &chirpBuf); // Write the file to program cache chirpFile = fopen([pathNSString cStringUsingEncoding: 1], "wb"); if ( chirpFile ) { fwrite(chirpBuf, sizeof(uint8_t), bufSize, chirpFile); fclose(chirpFile); } // Create system sound objects representing the sound files AudioServicesCreateSystemSoundID(mySoundURL, &sampleWorkout[loop].soundID); CFRelease(mySoundURL); } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Internal Error" message: @"Unable To Create File URL" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } closeChirpLibrary(); /* ALERT!! The Chirp Workout Audio Files are now ready for use... This sample presents the basic usage of my Chirp iOS Framework. You'll want to improve error checking and optimize this for the specifics of your application. Use the chirp files by creating a timer and periodically playing them */ self.nowPlaying = 0; AudioServicesPlaySystemSound(sampleWorkout[self.nowPlaying].soundID); self.chirpTimer = [NSTimer scheduledTimerWithTimeInterval: sampleWorkout[self.nowPlaying].duration target: self selector: @selector (playChirp:) userInfo: nil repeats: NO]; [self.information setBorderStyle: UITextBorderStyleNone]; [self.information setBackgroundColor: [UIColor lightGrayColor]]; [self.information setTextAlignment: UITextAlignmentCenter]; [self.information setText: [NSString stringWithFormat: @"Just played chirp %2d of %2d. Delaying %d sec", self.nowPlaying+1, SEGMENTS_IN_SAMPLE, sampleWorkout[self.nowPlaying].duration]]; } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Error opening Chirp Framework" message: @"openChirpLibrary() returned an error" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } - (IBAction) makeSdWorkout: (id) sender { int loop; int count; int fResult = 0; // File operation results int runningTime; // Time for next segment in seconds uint32_t res; // Result (error code) of function called uint32_t fileSize; uint8_t* sdBuf; FILE* sdFile; NSString* sdName; NSString* sdPath; NSString* cacheDirectory; // Don't allow a chirp workout to continue running... if ( self.chirpTimer ) { [self.chirpTimer invalidate]; self.chirpTimer = nil; } // Grab access to the cacheDirectory (where we store the SD file) NSArray* filePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); cacheDirectory = [filePaths objectAtIndex: 0]; copySampleWorkout(sampleWorkout); // Get our predefined sample workout res = openSdLibrary(); // ...and start using it if ( res == SdERR_NONE ) { [self.libVersion setBorderStyle: UITextBorderStyleNone]; [self.libVersion setBackgroundColor: [UIColor yellowColor]]; [self.libVersion setTextAlignment: UITextAlignmentCenter]; [self.libVersion setText: [NSString stringWithFormat:@"Using SD Framework Version %8X", getSdLibraryVersion()]]; res = startSdWorkout(); loop = 0; runningTime = 0; while ( (res == SdERR_NONE) && (loop < SEGMENTS_IN_SAMPLE) ) { if ( sampleWorkout[loop].duration == ENDSAMP ) { // This is the last segment. Stop the treadmill res = addSegmentToSdWorkout((uint16_t) runningTime, 0.0, 0.0); } else res = addSegmentToSdWorkout((uint16_t) runningTime, sampleWorkout[loop].speed, sampleWorkout[loop].incline); runningTime += sampleWorkout[loop].duration; loop++; } if ( res == SdERR_NONE ) { res = finishSdWorkout(&fileSize, &sdBuf); if ( res == SdERR_NONE ) { // Write the SD workout file to program cache sdName = [NSString stringWithFormat:@"%@", @"sampleworkout"]; sdPath = [cacheDirectory stringByAppendingPathComponent: sdName]; sdFile = fopen([sdPath cStringUsingEncoding: 1], "wb"); if ( sdFile ) { count = fwrite(sdBuf, sizeof(uint8_t), fileSize, sdFile); fclose(sdFile); if ( count != fileSize ) fResult = 1; // ...anything non-zero } else fResult = 2; // ...anything non-zero if ( fResult == 0 ) { /* The SD Workout File, whose path is 'sdPath', is ready for use Do something with it, like emailing it... */ [self.information setBorderStyle: UITextBorderStyleNone]; [self.information setBackgroundColor: [UIColor lightGrayColor]]; [self.information setTextAlignment: UITextAlignmentCenter]; [self.information setText: [NSString stringWithFormat: @"SD file created. E-Mail it (or whatever)"]]; } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Error:\nA problem was encountered while writing a file" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Error:\nA problem was encountered writing this workout" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"ERROR:\nA problem was encountered creating this workout" message: @"" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } closeSdLibrary(); // File written, free() memory... } else { UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Error opening SD Framework" message: @"openSdLibrary() returned an error" delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: nil]; [alert show]; [alert release]; } } @end