Commits

Tim Hibbard committed 70449a3

added ability to search by call sign or station name

  • Participants
  • Parent commits 4e481c2

Comments (0)

Files changed (31)

File Christian FM.xcodeproj/project.pbxproj

 /* Begin PBXBuildFile section */
 		12044B0A16022DA40083FF62 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 12044B0916022DA40083FF62 /* Default-568h@2x.png */; };
 		1233EED415B4FD6600D67A41 /* ChristianRadioFinder.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 1233EED315B4FD6600D67A41 /* ChristianRadioFinder.sqlite */; };
+		1236EA0616270344002C1845 /* ASPlaylist.m in Sources */ = {isa = PBXBuildFile; fileRef = 1236EA0116270344002C1845 /* ASPlaylist.m */; };
+		1236EA0716270344002C1845 /* AudioStreamer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1236EA0316270344002C1845 /* AudioStreamer.m */; };
+		1236EA0816270344002C1845 /* iPhoneStreamer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1236EA0516270344002C1845 /* iPhoneStreamer.m */; };
 		123E270515E94D82004E9925 /* WebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 123E270415E94D82004E9925 /* WebViewController.m */; };
 		124852AA15CDB4090036C49D /* MapDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 124852A915CDB4090036C49D /* MapDetailViewController.m */; };
 		124852B115CDFCC60036C49D /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 124852B015CDFCC60036C49D /* MapKit.framework */; };
 		125DB8AC15B277FC00A19A68 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 125DB8AB15B277FC00A19A68 /* CoreLocation.framework */; };
 		125E332515B5E616004417B6 /* ios_distribution.cer in Resources */ = {isa = PBXBuildFile; fileRef = 125E332415B5E616004417B6 /* ios_distribution.cer */; };
 		125E332915B5E882004417B6 /* TimHibbard (1).mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 125E332815B5E882004417B6 /* TimHibbard (1).mobileprovision */; };
+		125F3A29162650AE00CEEBF5 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 125F3A28162650AE00CEEBF5 /* AudioToolbox.framework */; };
+		125F3A2F162662F000CEEBF5 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 125F3A2E162662F000CEEBF5 /* AVFoundation.framework */; };
 		126605CE160AA4D5008444D3 /* Facebook-Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 126605CD160AA4D5008444D3 /* Facebook-Icon.png */; };
 		126605D1160AA8CD008444D3 /* drawingpin1_blue.png in Resources */ = {isa = PBXBuildFile; fileRef = 126605D0160AA8CD008444D3 /* drawingpin1_blue.png */; };
 		126605D3160ACC9A008444D3 /* EventKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 126605D2160ACC9A008444D3 /* EventKit.framework */; };
 		12BE3CB215B3BA5600AFCF25 /* grayArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 12BE3CA615B3BA5600AFCF25 /* grayArrow@2x.png */; };
 		12BE3CB315B3BA5600AFCF25 /* whiteArrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 12BE3CA715B3BA5600AFCF25 /* whiteArrow.png */; };
 		12BE3CB415B3BA5600AFCF25 /* whiteArrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 12BE3CA815B3BA5600AFCF25 /* whiteArrow@2x.png */; };
+		12D4F9191624F84E00F02E8F /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 12D4F9031624F84E00F02E8F /* .gitignore */; };
+		12D4F91A1624F84E00F02E8F /* Appirater.m in Sources */ = {isa = PBXBuildFile; fileRef = 12D4F9051624F84E00F02E8F /* Appirater.m */; };
+		12D4F91B1624F84E00F02E8F /* AppiraterLocalizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 12D4F9071624F84E00F02E8F /* AppiraterLocalizable.strings */; };
+		12D4F91C1624F84E00F02E8F /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 12D4F9141624F84E00F02E8F /* README.md */; };
+		12D4F91F1624F8E500F02E8F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12D4F91E1624F8E500F02E8F /* CFNetwork.framework */; };
+		12D4F9211624F8EF00F02E8F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12D4F9201624F8EF00F02E8F /* SystemConfiguration.framework */; };
+		12D60C5C1625A8B8007B50A7 /* AboutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 12D60C5B1625A8B8007B50A7 /* AboutViewController.m */; };
+		12D60C5E1625B95D007B50A7 /* engraph.png in Resources */ = {isa = PBXBuildFile; fileRef = 12D60C5D1625B95D007B50A7 /* engraph.png */; };
 		12DB227215CEC4C700F9F7D6 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DB227115CEC4C700F9F7D6 /* MessageUI.framework */; };
 		12DD2C5915EF01D600BF296F /* AddNewStationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 12DD2C5815EF01D600BF296F /* AddNewStationViewController.m */; };
 		12DD2C6115EFB23800BF296F /* StationMapViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 12DD2C6015EFB23800BF296F /* StationMapViewController.m */; };
 		12044B0916022DA40083FF62 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = "<group>"; };
 		12284B1415D6EF4E000CA178 /* Stations With Stream.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Stations With Stream.xcdatamodel"; sourceTree = "<group>"; };
 		1233EED315B4FD6600D67A41 /* ChristianRadioFinder.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = ChristianRadioFinder.sqlite; sourceTree = "<group>"; };
+		1236EA0016270344002C1845 /* ASPlaylist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPlaylist.h; sourceTree = "<group>"; };
+		1236EA0116270344002C1845 /* ASPlaylist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPlaylist.m; sourceTree = "<group>"; };
+		1236EA0216270344002C1845 /* AudioStreamer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioStreamer.h; sourceTree = "<group>"; };
+		1236EA0316270344002C1845 /* AudioStreamer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioStreamer.m; sourceTree = "<group>"; };
+		1236EA0416270344002C1845 /* iPhoneStreamer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iPhoneStreamer.h; sourceTree = "<group>"; };
+		1236EA0516270344002C1845 /* iPhoneStreamer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iPhoneStreamer.m; sourceTree = "<group>"; };
+		1236EA0916270BFD002C1845 /* Stations With BackgroundStream.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Stations With BackgroundStream.xcdatamodel"; sourceTree = "<group>"; };
 		123E270315E94D82004E9925 /* WebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebViewController.h; sourceTree = "<group>"; };
 		123E270415E94D82004E9925 /* WebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewController.m; sourceTree = "<group>"; };
 		124852A815CDB4090036C49D /* MapDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapDetailViewController.h; sourceTree = "<group>"; };
 		125DB8AB15B277FC00A19A68 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
 		125E332415B5E616004417B6 /* ios_distribution.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = ios_distribution.cer; sourceTree = "<group>"; };
 		125E332815B5E882004417B6 /* TimHibbard (1).mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "TimHibbard (1).mobileprovision"; sourceTree = "<group>"; };
+		125F3A28162650AE00CEEBF5 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
+		125F3A2E162662F000CEEBF5 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
 		126605CD160AA4D5008444D3 /* Facebook-Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Facebook-Icon.png"; sourceTree = "<group>"; };
 		126605D0160AA8CD008444D3 /* drawingpin1_blue.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = drawingpin1_blue.png; sourceTree = "<group>"; };
 		126605D2160ACC9A008444D3 /* EventKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = EventKit.framework; path = System/Library/Frameworks/EventKit.framework; sourceTree = SDKROOT; };
 		12BE3CA615B3BA5600AFCF25 /* grayArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "grayArrow@2x.png"; sourceTree = "<group>"; };
 		12BE3CA715B3BA5600AFCF25 /* whiteArrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = whiteArrow.png; sourceTree = "<group>"; };
 		12BE3CA815B3BA5600AFCF25 /* whiteArrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "whiteArrow@2x.png"; sourceTree = "<group>"; };
+		12D4F9031624F84E00F02E8F /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
+		12D4F9041624F84E00F02E8F /* Appirater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Appirater.h; sourceTree = "<group>"; };
+		12D4F9051624F84E00F02E8F /* Appirater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Appirater.m; sourceTree = "<group>"; };
+		12D4F9061624F84E00F02E8F /* AppiraterDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppiraterDelegate.h; sourceTree = "<group>"; };
+		12D4F9081624F84E00F02E8F /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9091624F84E00F02E8F /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90A1624F84E00F02E8F /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90B1624F84E00F02E8F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90C1624F84E00F02E8F /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90D1624F84E00F02E8F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90E1624F84E00F02E8F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F90F1624F84E00F02E8F /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9101624F84E00F02E8F /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9111624F84E00F02E8F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9121624F84E00F02E8F /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9131624F84E00F02E8F /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9141624F84E00F02E8F /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
+		12D4F9151624F84E00F02E8F /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9161624F84E00F02E8F /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/AppiraterLocalizable.strings; sourceTree = "<group>"; };
+		12D4F9171624F84E00F02E8F /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AppiraterLocalizable.strings"; sourceTree = "<group>"; };
+		12D4F9181624F84E00F02E8F /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/AppiraterLocalizable.strings"; sourceTree = "<group>"; };
+		12D4F91E1624F8E500F02E8F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
+		12D4F9201624F8EF00F02E8F /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
+		12D60C5A1625A8B8007B50A7 /* AboutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AboutViewController.h; sourceTree = "<group>"; };
+		12D60C5B1625A8B8007B50A7 /* AboutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AboutViewController.m; sourceTree = "<group>"; };
+		12D60C5D1625B95D007B50A7 /* engraph.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = engraph.png; sourceTree = "<group>"; };
 		12DB227115CEC4C700F9F7D6 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
 		12DD2C5715EF01D600BF296F /* AddNewStationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddNewStationViewController.h; sourceTree = "<group>"; };
 		12DD2C5815EF01D600BF296F /* AddNewStationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddNewStationViewController.m; sourceTree = "<group>"; };
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				125F3A2F162662F000CEEBF5 /* AVFoundation.framework in Frameworks */,
+				125F3A29162650AE00CEEBF5 /* AudioToolbox.framework in Frameworks */,
+				12D4F9211624F8EF00F02E8F /* SystemConfiguration.framework in Frameworks */,
+				12D4F91F1624F8E500F02E8F /* CFNetwork.framework in Frameworks */,
 				126605D3160ACC9A008444D3 /* EventKit.framework in Frameworks */,
 				12DB227215CEC4C700F9F7D6 /* MessageUI.framework in Frameworks */,
 				124852B115CDFCC60036C49D /* MapKit.framework in Frameworks */,
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		1236E9FF16270344002C1845 /* AudioStreamer */ = {
+			isa = PBXGroup;
+			children = (
+				1236EA0016270344002C1845 /* ASPlaylist.h */,
+				1236EA0116270344002C1845 /* ASPlaylist.m */,
+				1236EA0216270344002C1845 /* AudioStreamer.h */,
+				1236EA0316270344002C1845 /* AudioStreamer.m */,
+				1236EA0416270344002C1845 /* iPhoneStreamer.h */,
+				1236EA0516270344002C1845 /* iPhoneStreamer.m */,
+			);
+			path = AudioStreamer;
+			sourceTree = "<group>";
+		};
 		12514E3115B5C9C800B4A938 /* reicons */ = {
 			isa = PBXGroup;
 			children = (
 		125DB84C15B1BF6A00A19A68 = {
 			isa = PBXGroup;
 			children = (
+				12D4F9021624F84E00F02E8F /* arashpayan-appirater-bc862ba */,
 				12044B0916022DA40083FF62 /* Default-568h@2x.png */,
 				125DB86115B1BF6A00A19A68 /* Christian FM */,
 				125DB88215B1BF6A00A19A68 /* Christian FMTests */,
 		125DB85A15B1BF6A00A19A68 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				125F3A2E162662F000CEEBF5 /* AVFoundation.framework */,
+				125F3A28162650AE00CEEBF5 /* AudioToolbox.framework */,
+				12D4F9201624F8EF00F02E8F /* SystemConfiguration.framework */,
+				12D4F91E1624F8E500F02E8F /* CFNetwork.framework */,
 				126605D2160ACC9A008444D3 /* EventKit.framework */,
 				12DB227115CEC4C700F9F7D6 /* MessageUI.framework */,
 				124852B015CDFCC60036C49D /* MapKit.framework */,
 		125DB86115B1BF6A00A19A68 /* Christian FM */ = {
 			isa = PBXGroup;
 			children = (
+				1236E9FF16270344002C1845 /* AudioStreamer */,
 				126CFC8B15E875CA002E3B90 /* images */,
 				127E5BD415E1355100B71EF7 /* Managers */,
 				1252492F15BCEADE0017B761 /* TableViewCells */,
 				12DD2C5815EF01D600BF296F /* AddNewStationViewController.m */,
 				12DD2C5F15EFB23800BF296F /* StationMapViewController.h */,
 				12DD2C6015EFB23800BF296F /* StationMapViewController.m */,
+				12D60C5A1625A8B8007B50A7 /* AboutViewController.h */,
+				12D60C5B1625A8B8007B50A7 /* AboutViewController.m */,
 			);
 			name = iPhoneViews;
 			sourceTree = "<group>";
 		126CFC8B15E875CA002E3B90 /* images */ = {
 			isa = PBXGroup;
 			children = (
+				12D60C5D1625B95D007B50A7 /* engraph.png */,
 				126605D0160AA8CD008444D3 /* drawingpin1_blue.png */,
 				126605CD160AA4D5008444D3 /* Facebook-Icon.png */,
 				126CFC9015E91219002E3B90 /* f_logo.png */,
 			path = Resources;
 			sourceTree = "<group>";
 		};
+		12D4F9021624F84E00F02E8F /* arashpayan-appirater-bc862ba */ = {
+			isa = PBXGroup;
+			children = (
+				12D4F9031624F84E00F02E8F /* .gitignore */,
+				12D4F9041624F84E00F02E8F /* Appirater.h */,
+				12D4F9051624F84E00F02E8F /* Appirater.m */,
+				12D4F9061624F84E00F02E8F /* AppiraterDelegate.h */,
+				12D4F9071624F84E00F02E8F /* AppiraterLocalizable.strings */,
+				12D4F9141624F84E00F02E8F /* README.md */,
+			);
+			name = "arashpayan-appirater-bc862ba";
+			path = "../../Dropbox/Development/arashpayan-appirater-bc862ba";
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
 			knownRegions = (
 				en,
 				English,
+				cs,
+				da,
+				de,
+				es,
+				fr,
+				it,
+				ja,
+				ko,
+				nl,
+				pl,
+				pt,
+				ru,
+				sv,
+				"zh-Hans",
+				"zh-Hant",
 			);
 			mainGroup = 125DB84C15B1BF6A00A19A68;
 			productRefGroup = 125DB85815B1BF6A00A19A68 /* Products */;
 				12044B0A16022DA40083FF62 /* Default-568h@2x.png in Resources */,
 				126605CE160AA4D5008444D3 /* Facebook-Icon.png in Resources */,
 				126605D1160AA8CD008444D3 /* drawingpin1_blue.png in Resources */,
+				12D4F9191624F84E00F02E8F /* .gitignore in Resources */,
+				12D4F91B1624F84E00F02E8F /* AppiraterLocalizable.strings in Resources */,
+				12D4F91C1624F84E00F02E8F /* README.md in Resources */,
+				12D60C5E1625B95D007B50A7 /* engraph.png in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 				123E270515E94D82004E9925 /* WebViewController.m in Sources */,
 				12DD2C5915EF01D600BF296F /* AddNewStationViewController.m in Sources */,
 				12DD2C6115EFB23800BF296F /* StationMapViewController.m in Sources */,
+				12D4F91A1624F84E00F02E8F /* Appirater.m in Sources */,
+				12D60C5C1625A8B8007B50A7 /* AboutViewController.m in Sources */,
+				1236EA0616270344002C1845 /* ASPlaylist.m in Sources */,
+				1236EA0716270344002C1845 /* AudioStreamer.m in Sources */,
+				1236EA0816270344002C1845 /* iPhoneStreamer.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 			name = InfoPlist.strings;
 			sourceTree = "<group>";
 		};
+		12D4F9071624F84E00F02E8F /* AppiraterLocalizable.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				12D4F9081624F84E00F02E8F /* cs */,
+				12D4F9091624F84E00F02E8F /* da */,
+				12D4F90A1624F84E00F02E8F /* de */,
+				12D4F90B1624F84E00F02E8F /* en */,
+				12D4F90C1624F84E00F02E8F /* es */,
+				12D4F90D1624F84E00F02E8F /* fr */,
+				12D4F90E1624F84E00F02E8F /* it */,
+				12D4F90F1624F84E00F02E8F /* ja */,
+				12D4F9101624F84E00F02E8F /* ko */,
+				12D4F9111624F84E00F02E8F /* nl */,
+				12D4F9121624F84E00F02E8F /* pl */,
+				12D4F9131624F84E00F02E8F /* pt */,
+				12D4F9151624F84E00F02E8F /* ru */,
+				12D4F9161624F84E00F02E8F /* sv */,
+				12D4F9171624F84E00F02E8F /* zh-Hans */,
+				12D4F9181624F84E00F02E8F /* zh-Hant */,
+			);
+			name = AppiraterLocalizable.strings;
+			sourceTree = "<group>";
+		};
 /* End PBXVariantGroup section */
 
 /* Begin XCBuildConfiguration section */
 			children = (
 				12803D2015BD90290065285C /* Stations 2.xcdatamodel */,
 				12284B1415D6EF4E000CA178 /* Stations With Stream.xcdatamodel */,
+				1236EA0916270BFD002C1845 /* Stations With BackgroundStream.xcdatamodel */,
 				125DB89415B1BFBA00A19A68 /* Stations.xcdatamodel */,
 			);
-			currentVersion = 12284B1415D6EF4E000CA178 /* Stations With Stream.xcdatamodel */;
+			currentVersion = 1236EA0916270BFD002C1845 /* Stations With BackgroundStream.xcdatamodel */;
 			path = Stations.xcdatamodeld;
 			sourceTree = "<group>";
 			versionGroupType = wrapper.xcdatamodel;

File Christian FM.xcodeproj/project.xcworkspace/xcuserdata/timhibbard.xcuserdatad/UserInterfaceState.xcuserstate

Binary file modified.

File Christian Radio Finder/AboutViewController.h

+//
+//  AboutViewController.h
+//  Christian FM
+//
+//  Created by Tim Hibbard on 10/10/12.
+//
+//
+
+#import <UIKit/UIKit.h>
+#import "WebViewController.h"
+#import "Appirater.h"
+
+@interface AboutViewController : UITableViewController
+
+@end

File Christian Radio Finder/AboutViewController.m

+//
+//  AboutViewController.m
+//  Christian FM
+//
+//  Created by Tim Hibbard on 10/10/12.
+//
+//
+
+#import "AboutViewController.h"
+
+@interface AboutViewController ()
+
+@end
+
+@implementation AboutViewController
+
+- (id)initWithStyle:(UITableViewStyle)style
+{
+    self = [super initWithStyle:style];
+    if (self) {
+        // Custom initialization
+    }
+    return self;
+}
+
+- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
+    if (indexPath.section == 3) {
+        switch (indexPath.row) {
+            case 0:
+                [Appirater rateApp];
+                break;
+                
+            case 1:{
+                if (![self launchTwitter:@"CRLApp"]) {
+                    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://twitter.com/CRLApp"]];
+                }
+            }break;
+            case 2:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://www.facebook.com/pages/Christian-Radio-Locator/372509509486534"]];
+                break;
+            case 3:{
+                if (![self launchTwitter:@"timhibbard"]) {
+                    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://twitter.com/timhibbard"]];
+                }
+            case 4:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://timhibbard.com"]];
+                break;
+            case 5:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://engraph.com"]];
+                break;
+            }break;
+            default:
+                break;
+        }
+    }
+    
+    if (indexPath.section == 4) {
+        switch (indexPath.row) {
+            case 0:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://chelseablogs.com"]];
+                break;
+                
+            case 1:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/arashpayan/appirater/"]];
+                break;
+            case 2:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://radio-locator.com"]];
+                break;
+            case 3:
+                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/alexcrichton/AudioStreamer"]];
+                break;
+            default:
+                break;
+        }
+    }
+    
+    if (indexPath.section == 5) {
+        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+        [defaults setObject:@"Press and hold to search" forKey:@"mapHelpText"];
+        [defaults setObject:@"Swipe left to see more info" forKey:@"helpText"];
+        [defaults synchronize];
+        
+    }
+    
+    if (indexPath.section == 6) {
+        [self dismissModalViewControllerAnimated:YES];
+    }
+}
+
+#pragma mark - conditional app launchers
+
+- (BOOL) launchTwitter: (NSString *) twitterHandle{
+    
+    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"tweetbot:///user_profile/%@",twitterHandle]];
+    if ([[UIApplication sharedApplication] canOpenURL:url]) {
+        [[UIApplication sharedApplication] openURL:url];
+        return YES;
+    }
+    
+    url = [NSURL URLWithString:[NSString stringWithFormat:@"echofon:///user_timeline?%@",twitterHandle]];
+    if ([[UIApplication sharedApplication] canOpenURL:url]) {
+        [[UIApplication sharedApplication] openURL:url];
+        return YES;
+    }
+    
+    url = [NSURL URLWithString:[NSString stringWithFormat:@"twitter://user?screen_name=%@",twitterHandle]];
+    if ([[UIApplication sharedApplication] canOpenURL:url]) {
+        [[UIApplication sharedApplication] openURL:url];
+        return YES;
+    }
+    
+    return NO;
+} 
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    // Uncomment the following line to preserve selection between presentations.
+    // self.clearsSelectionOnViewWillAppear = NO;
+ 
+    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
+    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
+}
+
+- (void)didReceiveMemoryWarning
+{
+    [super didReceiveMemoryWarning];
+    // Dispose of any resources that can be recreated.
+}
+
+#pragma mark - Table view data source
+
+
+
+/*
+// Override to support conditional editing of the table view.
+- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    // Return NO if you do not want the specified item to be editable.
+    return YES;
+}
+*/
+
+/*
+// Override to support editing the table view.
+- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    if (editingStyle == UITableViewCellEditingStyleDelete) {
+        // Delete the row from the data source
+        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
+    }   
+    else if (editingStyle == UITableViewCellEditingStyleInsert) {
+        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
+    }   
+}
+*/
+
+/*
+// Override to support rearranging the table view.
+- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
+{
+}
+*/
+
+/*
+// Override to support conditional rearranging of the table view.
+- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    // Return NO if you do not want the item to be re-orderable.
+    return YES;
+}
+*/
+
+
+
+@end

File Christian Radio Finder/AddNewStationViewController.h

 @property (nonatomic) NSInteger locationHitCount;
 
 -(IBAction)submitButtonClicked:(id)sender;
+-(IBAction)cancelButtonClicked:(id)sender;
 -(IBAction)backgroundTouched:(id)sender;
 
 @end

File Christian Radio Finder/AddNewStationViewController.m

 //    return YES;
 //}
 
+- (void) cancelButtonClicked:(id)sender{
+    [self dismissModalViewControllerAnimated:YES];
+}
+
 
 - (void) submitButtonClicked:(id)sender{
     if ([MFMailComposeViewController canSendMail])

File Christian Radio Finder/AppDelegate.h

 //
 
 #import <UIKit/UIKit.h>
+#import "Appirater.h"
+#import "iPhoneStreamer.h"
 
 @interface AppDelegate : UIResponder <UIApplicationDelegate>{
     NSManagedObjectModel *managedObjectModel;
 @property (nonatomic, readonly) NSManagedObjectModel *managedObjectModel;   
 @property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext;   
 @property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
+@property (nonatomic, retain) iPhoneStreamer *streamer;
+@property (nonatomic, retain) NSString *currentlyPlayingUrl;
 
 - (NSString *) applicationDocumentsDirectory;
+- (void) playStream: (NSString *) urlString;
+- (void) playCurrentStream;
+- (void) destroyStream;
+- (void) pauseStream;
+- (BOOL) isLoadedAndPlaying;
+- (BOOL) isLoadedAndPaused;
 
 @end

File Christian Radio Finder/AppDelegate.m

 @synthesize managedObjectModel;
 @synthesize managedObjectContext;
 @synthesize persistentStoreCoordinator;
+@synthesize streamer, currentlyPlayingUrl;
 
 - (NSManagedObjectContext *) managedObjectContext {
     if (managedObjectContext != nil) {
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
     // Override point for customization after application launch.
+    [Appirater setAppId:@"545689326"];
+    //[Appirater setDebug:YES];
 
     UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
 //    StationTableViewController *controller = (StationTableViewController *)self.window.rootViewController;
     StationTableViewController *controller = (StationTableViewController *)[[navigationController viewControllers] objectAtIndex:0];
     controller.managedObjectContext = self.managedObjectContext;
     [self createEditableCopyOfDatabaseIfNeeded];
+    [Appirater appLaunched:YES];
     return YES;
 
 
 
 - (void)applicationWillEnterForeground:(UIApplication *)application
 {
+    [Appirater appEnteredForeground:YES];
     // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
 }
 
     // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
 }
 
+- (void)remoteControlReceivedWithEvent:(UIEvent *)theEvent{
+    if (theEvent.type == UIEventTypeRemoteControl) {
+        
+        switch (theEvent.subtype) {
+                
+            case UIEventSubtypeRemoteControlTogglePlayPause:{
+                if (!streamer) {
+                    return;
+                }
+                if (streamer.isWaiting) {
+                    return;
+                }
+                if (streamer.isPlaying) {
+                    [self pauseStream];
+                    return;
+                }
+                [self playCurrentStream];
+            }break;
+            default:
+                break;
+        }
+    }
+}
+
+- (BOOL)canBecomeFirstResponder {
+    return YES;
+}
+
+- (void) playCurrentStream{
+    [self playStream:currentlyPlayingUrl];
+}
+
+- (void) playStream: (NSString *) urlString{
+    if ([urlString isEqualToString:currentlyPlayingUrl]) {
+        if (streamer.isWaiting || streamer.isPlaying) {
+            return;
+        }
+        [streamer play];
+        return;
+    }
+
+    [self destroyStream];
+    
+    NSURL *url = [NSURL URLWithString:urlString];
+    
+    streamer = (iPhoneStreamer*)[iPhoneStreamer streamWithURL:url];
+    
+    BOOL startResult = [streamer start];
+    if (startResult) {
+        [self setCurrentlyPlayingUrl:urlString];
+        [[NSNotificationCenter defaultCenter]
+         addObserver:self
+         selector:@selector(stateChanged:)
+         name:ASStatusChangedNotification
+         object:streamer];
+        
+        
+        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
+        [self becomeFirstResponder];
+    }
+    
+}
+
+- (void) destroyStream{
+    if (!streamer) {
+        return;
+    }
+    [[NSNotificationCenter defaultCenter]
+     removeObserver:self
+     name:ASStatusChangedNotification
+     object:streamer];
+    [streamer stop];
+    streamer = nil;
+    currentlyPlayingUrl = [NSString alloc];
+    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
+    [self resignFirstResponder];
+}
+
+- (BOOL) isLoadedAndPaused{
+    if (!streamer) {
+        return NO;
+    }
+    if (streamer.isPaused) {
+        return YES;
+    }
+    return NO;
+}
+
+- (BOOL) isLoadedAndPlaying{
+    if (!streamer) {
+        return NO;
+    }
+    if (streamer.isPlaying || streamer.isWaiting) {
+        return YES;
+    }
+    return NO;
+}
+
+- (void) pauseStream{
+    if (!streamer) {
+        return;
+    }
+    if (streamer.isWaiting || streamer.isPaused) {
+        return;
+    }
+    [streamer pause];
+}
+
+- (void) stateChanged:(NSNotification*) not {
+    AudioStreamer *stream = [not object];
+    if ([stream errorCode] != AS_NO_ERROR) {
+        // handle the error via a UI, retrying the stream, etc.
+    } else if ([stream isPlaying]) {
+        
+    } else if ([stream isPaused]) {
+
+    } else if ([stream isDone]) {
+
+    } else {
+        // stream is waiting for data, probably nothing to do
+    }
+}
+
+
+
 @end

File Christian Radio Finder/AudioStreamer/ASPlaylist.h

+//
+//  ASPlaylist.h
+//  AudioStreamer
+//
+//  Created by Alex Crichton on 8/21/12.
+//
+//
+
+#import "AudioStreamer.h"
+
+extern NSString * const ASNewSongPlaying;
+extern NSString * const ASNoSongsLeft;
+extern NSString * const ASRunningOutOfSongs;
+extern NSString * const ASCreatedNewStream;
+extern NSString * const ASStreamError;
+
+/**
+ * The ASPlaylist class is intended to be a wrapper around the AudioStreamer
+ * class for a more robust interface if one is desired. It also manages a queue
+ * of songs to play and automatically switches from one song to the next when
+ * playback finishes.
+ */
+@interface ASPlaylist : NSObject {
+  BOOL retrying;              /* Are we retrying the current url? */
+  BOOL nexting;               /* Are we in the middle of nexting? */
+  BOOL volumeSet;             /* TRUE if the volume has been set on the stream */
+  double lastKnownSeekTime;   /* time to seek to */
+  double volume;              /* volume for all streams on this playlist */
+
+  NSInteger tries;            /* # of retry attempts */
+  NSMutableArray *urls;       /* list of URLs to play */
+  AudioStreamer *stream;      /* stream that is playing */
+}
+
+/**
+ * The currently playing URL.
+ *
+ * This is nil of no url has ever been playing.
+ */
+@property NSURL *playing;
+
+/** @name Managing the playlist */
+
+/**
+ * Start playing songs on the playlist, or resume playback.
+ *
+ * This will send out notifications for more songs if we're running low on songs
+ * or are out of songs completely to play.
+ */
+- (void) play;
+
+/**
+ * Pause playback on the playlist.
+ *
+ * This has no effect if the playlist is already paused or wasn't playing a song
+ */
+- (void) pause;
+
+/**
+ * Stops playing the current song and forgets about it.
+ *
+ * The song is stopped and internally all state about the song is thrown away
+ */
+- (void) stop;
+
+/**
+ * Goes to the next song in the playlist
+ *
+ * This can trigger notifications about songs running low or associated events.
+ */
+- (void) next;
+
+/** @name Interface to AudioStreamer */
+- (BOOL) isPaused;
+- (BOOL) isPlaying;
+- (BOOL) isIdle;
+- (BOOL) isError;
+- (void) setVolume:(double)volume;
+- (BOOL) duration:(double*)ret;
+- (BOOL) progress:(double*)ret;
+
+/** @name Miscellaneous */
+
+/**
+ * If the stream has stopped for a network error, this retries playing the
+ * stream
+ */
+- (void) retry;
+
+/**
+ * Removes all songs from the internal list of songs. This does not trigger
+ * notifications about songs running low.
+ */
+- (void) clearSongList;
+
+/**
+ * Adds a new song to the playlist, optionally starting playback.
+ */
+- (void) addSong:(NSURL*)url play:(BOOL)play;
+
+@end

File Christian Radio Finder/AudioStreamer/ASPlaylist.m

+//
+//  ASPlaylist.m
+//  AudioStreamer
+//
+//  Created by Alex Crichton on 8/21/12.
+//
+//
+
+#import "ASPlaylist.h"
+
+NSString * const ASCreatedNewStream  = @"ASCreatedNewStream";
+NSString * const ASNewSongPlaying    = @"ASNewSongPlaying";
+NSString * const ASNoSongsLeft       = @"ASNoSongsLeft";
+NSString * const ASRunningOutOfSongs = @"ASRunningOutOfSongs";
+NSString * const ASStreamError       = @"ASStreamError";
+
+@implementation ASPlaylist
+
+- (id) init {
+  if (!(self = [super init])) return nil;
+  urls = [NSMutableArray arrayWithCapacity:10];
+  return self;
+}
+
+- (void) dealloc {
+  [self stop];
+}
+
+- (void) clearSongList {
+  [urls removeAllObjects];
+}
+
+- (void) addSong:(NSURL*)url play:(BOOL)play {
+  [urls addObject:url];
+
+  if (play && ![stream isPlaying]) {
+    [self play];
+  }
+}
+
+- (void) setAudioStream {
+  if (stream != nil) {
+    [[NSNotificationCenter defaultCenter]
+        removeObserver:self
+                  name:nil
+                object:stream];
+    [stream stop];
+  }
+  stream = [AudioStreamer streamWithURL: _playing];
+  [[NSNotificationCenter defaultCenter]
+        postNotificationName:ASCreatedNewStream
+                      object:self
+                    userInfo:@{@"stream": stream}];
+  volumeSet = [stream setVolume:volume];
+
+  /* Watch for error notifications */
+  [[NSNotificationCenter defaultCenter]
+    addObserver:self
+       selector:@selector(playbackStateChanged:)
+           name:ASStatusChangedNotification
+         object:stream];
+  [[NSNotificationCenter defaultCenter]
+    addObserver:self
+       selector:@selector(bitrateReady:)
+           name:ASBitrateReadyNotification
+         object:stream];
+}
+
+- (void)bitrateReady: (NSNotification*)notification {
+  NSAssert([notification object] == stream,
+           @"Should only receive notifications for the current stream");
+  if (lastKnownSeekTime == 0)
+    return;
+  if (![stream seekToTime:lastKnownSeekTime])
+    return;
+  retrying = NO;
+  lastKnownSeekTime = 0;
+}
+
+- (void)playbackStateChanged: (NSNotification *)notification {
+  NSAssert([notification object] == stream,
+           @"Should only receive notifications for the current stream");
+  if (!volumeSet) {
+    volumeSet = [stream setVolume:volume];
+  }
+
+  int code = [stream errorCode];
+  if (code != 0) {
+    /* If we've hit an error, then we want to record out current progress into
+       the song. Only do this if we're not in the process of retrying to
+       establish a connection, so that way we don't blow away the original
+       progress from when the error first happened */
+    if (!retrying) {
+      if (![stream progress:&lastKnownSeekTime]) {
+        lastKnownSeekTime = 0;
+      }
+    }
+
+    /* If the network connection just outright failed, then we shouldn't be
+       retrying with a new auth token because it will never work for that
+       reason. Most likely this is some network trouble and we should have the
+       opportunity to hit a button to retry this specific connection so we can
+       at least hope to regain our current place in the song */
+    if (code == AS_NETWORK_CONNECTION_FAILED || code == AS_TIMED_OUT) {
+      [[NSNotificationCenter defaultCenter]
+            postNotificationName:ASStreamError
+                          object:self];
+
+    /* Otherwise, this might be because our authentication token is invalid, but
+       just in case, retry the current song automatically a few times before we
+       finally give up and clear our cache of urls (see below) */
+    } else {
+      [self retry];
+    }
+
+  /* When the stream has finished, move on to the next song */
+  } else if ([stream isDone]) {
+    if (!nexting) [self next];
+  }
+}
+
+- (void) retry {
+  if (tries > 2) {
+    /* too many retries means just skip to the next song */
+    [self clearSongList];
+    [self next];
+    return;
+  }
+  tries++;
+  retrying = YES;
+  [self setAudioStream];
+  [stream start];
+}
+
+- (void) play {
+  if (stream) {
+    [stream play];
+    return;
+  }
+
+  if ([urls count] == 0) {
+    [[NSNotificationCenter defaultCenter]
+          postNotificationName:ASNoSongsLeft
+                        object:self];
+    return;
+  }
+
+  _playing = urls[0];
+  [urls removeObjectAtIndex:0];
+  [self setAudioStream];
+  tries = 0;
+  [stream start];
+
+  [[NSNotificationCenter defaultCenter]
+        postNotificationName:ASNewSongPlaying
+                      object:self
+                    userInfo:@{@"url": _playing}];
+
+  if ([urls count] < 2) {
+    [[NSNotificationCenter defaultCenter]
+          postNotificationName:ASRunningOutOfSongs
+                        object:self];
+  }
+}
+
+- (void) pause { [stream pause]; }
+- (BOOL) isPaused { return [stream isPaused]; }
+- (BOOL) isPlaying { return [stream isPlaying]; }
+- (BOOL) isIdle { return [stream isDone]; }
+- (BOOL) isError { return [stream errorCode] != AS_NO_ERROR; }
+- (BOOL) progress:(double*)ret { return [stream progress:ret]; }
+- (BOOL) duration:(double*)ret { return [stream duration:ret]; }
+
+- (void) next {
+  if (nexting)
+    return;
+
+  nexting = YES;
+  lastKnownSeekTime = 0;
+  retrying = FALSE;
+  [self stop];
+  [self play];
+  nexting = NO;
+}
+
+- (void) stop {
+  nexting = YES;
+  [stream stop];
+  if (stream != nil) {
+    [[NSNotificationCenter defaultCenter]
+        removeObserver:self
+                  name:nil
+                object:stream];
+  }
+  stream = nil;
+  _playing = nil;
+}
+
+- (void) setVolume:(double)vol {
+  volumeSet = [stream setVolume:vol];
+  self->volume = vol;
+}
+
+@end

File Christian Radio Finder/AudioStreamer/AudioStreamer.h

+//
+//  AudioStreamer.h
+//  StreamingAudioPlayer
+//
+//  Created by Matt Gallagher on 27/09/08.
+//  Copyright 2008 Matt Gallagher. All rights reserved.
+//
+//  This software is provided 'as-is', without any express or implied
+//  warranty. In no event will the authors be held liable for any damages
+//  arising from the use of this software. Permission is granted to anyone to
+//  use this software for any purpose, including commercial applications, and to
+//  alter it and redistribute it freely, subject to the following restrictions:
+//
+//  1. The origin of this software must not be misrepresented; you must not
+//     claim that you wrote the original software. If you use this software
+//     in a product, an acknowledgment in the product documentation would be
+//     appreciated but is not required.
+//  2. Altered source versions must be plainly marked as such, and must not be
+//     misrepresented as being the original software.
+//  3. This notice may not be removed or altered from any source
+//     distribution.
+//
+
+/* This file has been heavily modified since its original distribution bytes
+   Alex Crichton for the Hermes project */
+
+#import <AudioToolbox/AudioToolbox.h>
+#import <Foundation/Foundation.h>
+
+/* Maximum number of packets which can be contained in one buffer */
+#define kAQMaxPacketDescs 512
+
+typedef enum {
+  AS_INITIALIZED = 0,
+  AS_WAITING_FOR_DATA,
+  AS_WAITING_FOR_QUEUE_TO_START,
+  AS_PLAYING,
+  AS_PAUSED,
+  AS_DONE,
+  AS_STOPPED
+} AudioStreamerState;
+
+typedef enum
+{
+  AS_NO_ERROR = 0,
+  AS_NETWORK_CONNECTION_FAILED,
+  AS_FILE_STREAM_GET_PROPERTY_FAILED,
+  AS_FILE_STREAM_SET_PROPERTY_FAILED,
+  AS_FILE_STREAM_SEEK_FAILED,
+  AS_FILE_STREAM_PARSE_BYTES_FAILED,
+  AS_FILE_STREAM_OPEN_FAILED,
+  AS_FILE_STREAM_CLOSE_FAILED,
+  AS_AUDIO_DATA_NOT_FOUND,
+  AS_AUDIO_QUEUE_CREATION_FAILED,
+  AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED,
+  AS_AUDIO_QUEUE_ENQUEUE_FAILED,
+  AS_AUDIO_QUEUE_ADD_LISTENER_FAILED,
+  AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED,
+  AS_AUDIO_QUEUE_START_FAILED,
+  AS_AUDIO_QUEUE_PAUSE_FAILED,
+  AS_AUDIO_QUEUE_BUFFER_MISMATCH,
+  AS_AUDIO_QUEUE_DISPOSE_FAILED,
+  AS_AUDIO_QUEUE_STOP_FAILED,
+  AS_AUDIO_QUEUE_FLUSH_FAILED,
+  AS_AUDIO_STREAMER_FAILED,
+  AS_GET_AUDIO_TIME_FAILED,
+  AS_AUDIO_BUFFER_TOO_SMALL,
+  AS_TIMED_OUT
+} AudioStreamerErrorCode;
+
+typedef enum {
+  AS_DONE_STOPPED,
+  AS_DONE_ERROR,
+  AS_DONE_EOF,
+  AS_NOT_DONE
+} AudioStreamerDoneReason;
+
+extern NSString * const ASStatusChangedNotification;
+extern NSString * const ASBitrateReadyNotification;
+
+struct queued_packet;
+
+/**
+ * This class is implemented on top of Apple's AudioQueue framework. This
+ * framework is much too low-level for must use cases, so this class
+ * encapsulates the functionality to provide a nicer interface. The interface
+ * still requires some management, but it is far more sane than dealing with the
+ * AudioQueue structures yourself.
+ *
+ * This class is essentially a pipeline of three components to get audio to the
+ * speakers:
+ *
+ *              CFReadStream => AudioFileStream => AudioQueue
+ *
+ * ### CFReadStream
+ *
+ * The method of reading HTTP data is using the low-level CFReadStream class
+ * because it allows configuration of proxies and scheduling/rescheduling on the
+ * event loop. All data read from the HTTP stream is piped into the
+ * AudioFileStream which then parses all of the data. This stage of the pipeline
+ * also flags that events are happening to prevent a timeout. All network
+ * activity occurs on the thread which started the audio stream.
+ *
+ * ### AudioFileStream
+ *
+ * This stage is implemented by Apple frameworks, and parses all audio data.  It
+ * is composed of two callbacks which receive data. The first callback invoked
+ * in series is one which is notified whenever a new property is known about the
+ * audio stream being received. Once all properties have been read, the second
+ * callback beings to be invoked, and this callback is responsible for dealing
+ * with packets.
+ *
+ * The second callback is invoked whenever complete "audio packets" are
+ * available to send to the audio queue. This stage is invoked on the call stack
+ * of the stream which received the data (synchronously with receiving the
+ * data).
+ *
+ * Packets received are buffered in a static set of buffers allocated by the
+ * audio queue instance. When a buffer is full, it is committed to the audio
+ * queue, and then the next buffer is moved on to. Multiple packets can possibly
+ * fit in one buffer. When committing a buffer, if there are no more buffers
+ * available, then the http read stream is unscheduled from the run loop and all
+ * currently received data is stored aside for later processing.
+ *
+ * ### AudioQueue
+ *
+ * This final stage is also implemented by Apple, and receives all of the full
+ * buffers of data from the AudioFileStream's parsed packets. The implementation
+ * manages its own set of threads, but callbacks are invoked on the main thread.
+ * The two callbacks that the audio stream is interested in are playback state
+ * changing and audio buffers being freed.
+ *
+ * When a buffer is freed, then it is marked as so, and if the stream was
+ * waiting for a buffer to be freed a message to empty the queue as much as
+ * possible is sent to the main thread's run loop. Otherwise no extra action
+ * need be performed.
+ *
+ * The main purpose of knowing when the playback state changes is to change the
+ * state of the player accordingly.
+ *
+ * ## Errors
+ *
+ * There are a large number of places where error can happen, and the stream can
+ * bail out at any time with an error. Each error has its own code and
+ * corresponding string representation. Any error will halt the entire audio
+ * stream and cease playback. Some errors might want to be handled by the
+ * manager of the AudioStreamer class, but others normally indicate that the
+ * remote stream just won't work.  Occasionally errors might reflect a lack of
+ * local resources.
+ *
+ * Error information can be learned from the errorCode property and the
+ * stringForErrorCode: method.
+ *
+ * ## Seeking
+ *
+ * To seek inside an audio stream, the bit rate must be known along with some
+ * other metadata, but this is not known until after the stream has started.
+ * For this reason the seek can fail if not enough data is known yet.
+ *
+ * If a seek succeeds, however, the actual method of doing so is as follows.
+ * First, open a stream at position 0 and collect data about the stream, when
+ * the seek is requested, cancel the stream and re-open the connection with the
+ * proper byte offset. This second stream is then used to put data through the
+ * pipelines.
+ *
+ * ## Example usage
+ *
+ * An audio stream is a one-shot thing. Once initialized, the source cannot be
+ * changed and a single audio stream cannot be re-used. To do this, multiple
+ * AudioStreamer objects need to be created/managed.
+ */
+@interface AudioStreamer : NSObject {
+  /* Properties specified before the stream starts. None of these properties
+   * should be changed after the stream has started or otherwise it could cause
+   * internal inconsistencies in the stream. Detail explanations of each
+   * property can be found in the source */
+  NSURL           *url;
+  int             proxyType;  /* defaults to whatever the system says */
+  NSString        *proxyHost;
+  int             proxyPort;
+  AudioFileTypeID fileType;
+  UInt32          bufferSize; /* attempted to be guessed, but fallback here */
+  UInt32          bufferCnt;
+  BOOL            bufferInfinite;
+  int             timeoutInterval;
+
+  /* Creates as part of the [start] method */
+  CFReadStreamRef stream;
+
+  /* Timeout management */
+  NSTimer *timeout; /* timer managing the timeout event */
+  BOOL unscheduled; /* flag if the http stream is unscheduled */
+  BOOL rescheduled; /* flag if the http stream was rescheduled */
+  int events;       /* events which have happened since the last tick */
+
+  /* Once the stream has bytes read from it, these are created */
+  NSDictionary *httpHeaders;
+  AudioFileStreamID audioFileStream;
+
+  /* The audio file stream will fill in these parameters */
+  UInt64 fileLength;         /* length of file, set from http headers */
+  UInt64 dataOffset;         /* offset into the file of the start of stream */
+  UInt64 audioDataByteCount; /* number of bytes of audio data in file */
+  AudioStreamBasicDescription asbd; /* description of audio */
+
+  /* Once properties have been read, packets arrive, and the audio queue is
+     created once the first packet arrives */
+  AudioQueueRef audioQueue;
+  UInt32 packetBufferSize;  /* guessed from audioFileStream */
+
+  /* When receiving audio data, raw data is placed into these buffers. The
+   * buffers are essentially a "ring buffer of buffers" as each buffer is cycled
+   * through and then freed when not in use. Each buffer can contain one or many
+   * packets, so the packetDescs array is a list of packets which describes the
+   * data in the next pending buffer (used to enqueue data into the AudioQueue
+   * structure */
+  AudioQueueBufferRef *buffers;
+  AudioStreamPacketDescription packetDescs[kAQMaxPacketDescs];
+  UInt32 packetsFilled;         /* number of valid entries in packetDescs */
+  UInt32 bytesFilled;           /* bytes in use in the pending buffer */
+  unsigned int fillBufferIndex; /* index of the pending buffer */
+  BOOL *inuse;                  /* which buffers have yet to be processed */
+  UInt32 buffersUsed;           /* Number of buffers in use */
+
+  /* cache state (see above description) */
+  bool waitingOnBuffer;
+  struct queued_packet *queued_head;
+  struct queued_packet *queued_tail;
+
+  /* Internal metadata about errors and state */
+  AudioStreamerState state_;
+  AudioStreamerErrorCode errorCode;
+  NSError *networkError;
+  OSStatus err;
+
+  /* Miscellaneous metadata */
+  bool discontinuous;        /* flag to indicate the middle of a stream */
+  UInt64 seekByteOffset;     /* position with the file to seek */
+  double seekTime;
+  bool seeking;              /* Are we currently in the process of seeking? */
+  double lastProgress;       /* last calculated progress point */
+  UInt64 processedPacketsCount;     /* bit rate calculation utility */
+  UInt64 processedPacketsSizeTotal; /* helps calculate the bit rate */
+  bool   bitrateNotification;       /* notified that the bitrate is ready */
+}
+
+/** @name Creating an audio stream */
+
+/**
+ * Allocate a new audio stream with the specified url
+ *
+ * The created stream has not started playback. This gives an opportunity to
+ * configure the rest of the stream as necessary. To start playback, send the
+ * stream an explicit 'start' message.
+ *
+ * @param url the remote source of audio
+ * @return the stream to configure and being playback with
+ */
++ (AudioStreamer*) streamWithURL:(NSURL*)url;
+
+/** @name Properties of the audio stream */
+
+/**
+ * If an error occurs on the stream, then this variable is set with the code
+ * corresponding to the error
+ *
+ * By default this is AS_NO_ERROR.
+ */
+@property AudioStreamerErrorCode errorCode;
+
+/**
+ * Converts an error code to a string
+ *
+ * @param anErrorCode the code to convert, usually from the errorCode field
+ * @return the string description of the error code (as best as possible)
+ */
++ (NSString*) stringForErrorCode:(AudioStreamerErrorCode)anErrorCode;
+
+/**
+ * Headers received from the remote source
+ *
+ * Used to determine file size, but other information may be useful as well
+ */
+@property (readonly) NSDictionary *httpHeaders;
+
+/* TODO: get rid of this */
+@property (readonly) NSError *networkError;
+
+/**
+ * The remote resource that this stream is playing, this is a read-only property
+ * and cannot be changed after creation
+ */
+@property (readonly) NSURL *url;
+
+/**
+ * The number of audio buffers to have
+ *
+ * Each audio buffer contains one or more packets of audio data. This amount is
+ * only relevant if infinite buffering is turned off. This is the amount of data
+ * which is stored in memory while playing. Once this memory is full, the remote
+ * connection will not be read and will not receive any more data until one of
+ * the buffers becomes available.
+ *
+ * With infinite buffering turned on, this number should be at least 3 or so to
+ * make sure that there's always data to be read. With infinite buffering turned
+ * off, this should be a number to not consume too much memory, but to also keep
+ * up with the remote data stream. The incoming data should always be able to
+ * stay ahead of these buffers being filled
+ *
+ * Default: 16
+ */
+@property (readwrite) UInt32 bufferCnt;
+
+/**
+ * The default size for each buffer allocated
+ *
+ * Each buffer's size is attempted to be guessed from the audio stream being
+ * received. This way each buffer is tuned for the audio stream itself. If this
+ * inferring of the buffer size fails, however, this is used as a fallback as
+ * how large each buffer should be.
+ *
+ * If you find that this is being used, then it should be coordinated with
+ * bufferCnt above to make sure that the audio stays responsive and slightly
+ * behind the HTTP stream
+ *
+ * Default: 2048
+ */
+@property (readwrite) UInt32 bufferSize;
+
+/**
+ * The file type of this audio stream
+ *
+ * This is an optional parameter. If not specified, then then the file type will
+ * be guessed. First, the MIME type of the response is used to guess the file
+ * type, and if that fails the extension on the url is used. If that fails as
+ * well, then the default is an MP3 stream.
+ *
+ * If this property is set, then no inferring is done and that file type is
+ * always used.
+ *
+ * Default: (guess)
+ */
+@property (readwrite) AudioFileTypeID fileType;
+
+/**
+ * Flag if to infinitely buffer data
+ *
+ * If this flag is set to NO, then a statically sized buffer is used as
+ * determined by bufferCnt and bufferSize above and the read stream will be
+ * descheduled when those fill up. This limits the bandwidth consumed to the
+ * remote source and also limits memory usage.
+ *
+ * If, however, you wish to hold the entire stream in memory, then you can set
+ * this flag to YES. In this state, the stream will be entirely downloaded,
+ * regardless if the buffers are full or not. This way if the network stream
+ * cuts off halfway through a song, the rest of the song will be downloaded
+ * locally to finish off. The next song might still be in trouble, however...
+ * With this turned on, memory usage will be higher because the entire stream
+ * will be downloaded as fast as possible, and the bandwidth to the remote will
+ * also be consumed. Depending on the situation, this might not be that bad of
+ * a problem.
+ *
+ * Default: NO
+ */
+@property (readwrite) BOOL bufferInfinite;
+
+/**
+ * Interval to consider timeout if no network activity is seen
+ *
+ * When downloading audio data from a remote source, this is the interval in
+ * which to consider it a timeout if no data is received. If the stream is
+ * paused, then that time interval is not counted. This only counts if we are
+ * waiting for data and an amount of time larger than this elapses.
+ *
+ * The units of this variable is seconds.
+ *
+ * Default: 10
+ */
+@property (readwrite) int timeoutInterval;
+
+/**
+ * Set an HTTP proxy for this stream
+ *
+ * @param host the address/hostname of the remote host
+ * @param port the port of the proxy
+ */
+- (void) setHTTPProxy:(NSString*)host port:(int)port;
+
+/**
+ * Set SOCKS proxy for this stream
+ *
+ * @param host the address/hostname of the remote host
+ * @param port the port of the proxy
+ */
+- (void) setSOCKSProxy:(NSString*)host port:(int)port;
+
+/** @name Management of the stream and testing state */
+
+/**
+ * Starts playback of this audio stream.
+ *
+ * This method can only be invoked once, and other methods will not work before
+ * this method has been invoked. All properties (like proxies) must be set
+ * before this method is invoked.
+ *
+ * @return YES if the stream was started, or NO if the stream was previously
+ *         started and this had no effect.
+ */
+- (BOOL) start;
+
+/**
+ * Stop all streams, cleaning up resources and preventing all further events
+ * from occurring.
+ *
+ * This method may be invoked at any time from any point of the audio stream as
+ * a signal of error happening. This method sets the state to AS_STOPPED if it
+ * isn't already AS_STOPPED or AS_DONE.
+ */
+- (void) stop;
+
+/**
+ * Pause the audio stream if playing
+ *
+ * @return YES if the audio stream was paused, or NO if it was not in the
+ *         AS_PLAYING state or an error occurred.
+ */
+- (BOOL) pause;
+
+/**
+ * Plays the audio stream if paused
+ *
+ * @return YES if the audio stream entered into the AS_PLAYING state, or NO if
+ *         any other error or bad state was encountered.
+ */
+- (BOOL) play;
+
+/**
+ * Tests whether the stream is playing
+ *
+ * @return YES if the stream is playing, or NO Otherwise
+ */
+- (BOOL) isPlaying;
+
+/**
+ * Tests whether the stream is paused
+ *
+ * A stream is not paused if it is waiting for data. A stream is paused if and
+ * only if it used to be playing, but the it was paused via the pause method.
+ *
+ * @return YES if the stream is paused, or NO Otherwise
+ */
+- (BOOL) isPaused;
+
+/**
+ * Tests whether the stream is waiting
+ *
+ * This could either mean that we're waiting on data from the network or waiting
+ * for some event with the AudioQueue instance.
+ *
+ * @return YES if the stream is waiting, or NO Otherwise
+ */
+- (BOOL) isWaiting;
+
+/**
+ * Tests whether the stream is done with all operation
+ *
+ * A stream can be 'done' if it either hits an error or consumes all audio data
+ * from the remote source.
+ *
+ * @return YES if the stream is done, or NO Otherwise
+ */
+- (BOOL) isDone;
+
+/**
+ * When isDone returns true, this will return the reason that the stream has
+ * been flagged as being done.
+ *
+ * @return the reason for the stream being done, or that it's not done.
+ */
+- (AudioStreamerDoneReason) doneReason;
+
+/** @name Calculated properties and modifying the stream (all can fail) */
+
+/**
+ * Seek to a specified time in the audio stream
+ *
+ * This can only happen once the bit rate of the stream is known because
+ * otherwise the byte offset to the stream is not known. For this reason the
+ * function can fail to actually seek.
+ *
+ * Additionally, seeking to a new time involves re-opening the audio stream with
+ * the remote source, although this is done under the hood.
+ *
+ * @param newSeekTime the time in seconds to seek to
+ * @return YES if the stream will be seeking, or NO if the stream did not have
+ *         enough information available to it to seek to the specified time.
+ */
+- (BOOL) seekToTime:(double)newSeekTime;
+
+/**
+ * Calculates the bit rate of the stream
+ *
+ * All packets received so far contribute to the calculation of the bit rate.
+ * This is used internally to determine other factors like duration and
+ * progress.
+ *
+ * @param ret the double to fill in with the bit rate on success.
+ * @return YES if the bit rate could be calculated with a high degree of
+ *         certainty, or NO if it could not be.
+ */
+- (BOOL) calculatedBitRate:(double*)ret;
+
+/**
+ * Attempt to set the volume on the audio queue
+ *
+ * @param volume the volume to set the stream to in the range 0.0-1.0 where 1.0
+ *        is the loudest and 0.0 is silent.
+ * @return YES if the volume was set, or NO if the audio queue wasn't to have
+ *         the volume ready to be set. When the state for this audio streamer
+ *         changes internally to have a stream, then setVolume: will work
+ */
+- (BOOL) setVolume:(double)volume;
+
+/**
+ * Calculates the duration of the audio stream in seconds
+ *
+ * Uses information about the size of the file and the calculated bit rate to
+ * determine the duration of the stream.
+ *
+ * @param ret where to fill in with the duration of the stream on success.
+ * @return YES if ret contains the duration of the stream, or NO if the duration
+ *         could not be determined. In the NO case, the contents of ret are
+ *         undefined
+ */
+- (BOOL) duration:(double*)ret;
+
+/**
+ * Calculate the progress into the stream, in seconds
+ *
+ * The AudioQueue instance is polled to determine the current time into the
+ * stream, and this is returned.
+ *
+ * @param ret a double which is filled in with the progress of the stream. The
+ *        contents are undefined if NO is returned.
+ * @return YES if the progress of the stream was determined, or NO if the
+ *         progress could not be determined at this time.
+ */
+- (BOOL) progress:(double*)ret;
+
+@end

File Christian Radio Finder/AudioStreamer/AudioStreamer.m

+//
+//  AudioStreamer.m
+//  StreamingAudioPlayer
+//
+//  Created by Matt Gallagher on 27/09/08.
+//  Copyright 2008 Matt Gallagher. All rights reserved.
+//
+//  This software is provided 'as-is', without any express or implied
+//  warranty. In no event will the authors be held liable for any damages
+//  arising from the use of this software. Permission is granted to anyone to
+//  use this software for any purpose, including commercial applications, and to
+//  alter it and redistribute it freely, subject to the following restrictions:
+//
+//  1. The origin of this software must not be misrepresented; you must not
+//     claim that you wrote the original software. If you use this software
+//     in a product, an acknowledgment in the product documentation would be
+//     appreciated but is not required.
+//  2. Altered source versions must be plainly marked as such, and must not be
+//     misrepresented as being the original software.
+//  3. This notice may not be removed or altered from any source
+//     distribution.
+//
+
+/* This file has been heavily modified since its original distribution bytes
+   Alex Crichton for the Hermes project */
+
+#import "AudioStreamer.h"
+
+#define BitRateEstimationMaxPackets 5000
+#define BitRateEstimationMinPackets 50
+
+#define PROXY_SYSTEM 0
+#define PROXY_SOCKS  1
+#define PROXY_HTTP   2
+
+/* Default number and size of audio queue buffers */
+#define kDefaultNumAQBufs 16
+#define kDefaultAQDefaultBufSize 2048
+
+#define CHECK_ERR(err, code) {                                                 \
+    if (err) { [self failWithErrorCode:code]; return; }                        \
+  }
+
+#if defined(DEBUG) && 0
+#define LOG(fmt, args...) NSLog(@"%s " fmt, __PRETTY_FUNCTION__, ##args)
+#else
+#define LOG(...)
+#endif
+
+typedef struct queued_packet {
+  AudioStreamPacketDescription desc;
+  struct queued_packet *next;
+  char data[];
+} queued_packet_t;
+
+NSString * const ASStatusChangedNotification = @"ASStatusChangedNotification";
+NSString * const ASBitrateReadyNotification = @"ASBitrateReadyNotification";
+
+@interface AudioStreamer ()
+
+- (void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream
+                     fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID
+                                  ioFlags:(UInt32 *)ioFlags;
+- (void)handleAudioPackets:(const void *)inInputData
+               numberBytes:(UInt32)inNumberBytes
+             numberPackets:(UInt32)inNumberPackets
+        packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions;
+- (void)handleBufferCompleteForQueue:(AudioQueueRef)inAQ
+                              buffer:(AudioQueueBufferRef)inBuffer;
+- (void)handlePropertyChangeForQueue:(AudioQueueRef)inAQ
+                          propertyID:(AudioQueuePropertyID)inID;
+
+- (void)handleReadFromStream:(CFReadStreamRef)aStream
+                   eventType:(CFStreamEventType)eventType;
+
+@end
+
+/* Woohoo, actual implementation now! */
+@implementation AudioStreamer
+
+@synthesize errorCode;
+@synthesize networkError;
+@synthesize httpHeaders;
+@synthesize url;
+@synthesize fileType;
+@synthesize bufferCnt;
+@synthesize bufferSize;
+@synthesize bufferInfinite;
+@synthesize timeoutInterval;
+
+/* AudioFileStream callback when properties are available */
+static void MyPropertyListenerProc(void *inClientData,
+                            AudioFileStreamID inAudioFileStream,
+                            AudioFileStreamPropertyID inPropertyID,
+                            UInt32 *ioFlags) {
+  AudioStreamer* streamer = (__bridge AudioStreamer *)inClientData;
+  [streamer handlePropertyChangeForFileStream:inAudioFileStream
+                         fileStreamPropertyID:inPropertyID
+                                      ioFlags:ioFlags];
+}
+
+/* AudioFileStream callback when packets are available */
+static void MyPacketsProc(void *inClientData, UInt32 inNumberBytes, UInt32
+                   inNumberPackets, const void *inInputData,
+                   AudioStreamPacketDescription  *inPacketDescriptions) {
+  AudioStreamer* streamer = (__bridge AudioStreamer *)inClientData;
+  [streamer handleAudioPackets:inInputData
+                   numberBytes:inNumberBytes
+                 numberPackets:inNumberPackets
+            packetDescriptions:inPacketDescriptions];
+}
+
+/* AudioQueue callback notifying that a buffer is done, invoked on AudioQueue's
+ * own personal threads, not the main thread */
+static void MyAudioQueueOutputCallback(void *inClientData, AudioQueueRef inAQ,
+                                AudioQueueBufferRef inBuffer) {
+  AudioStreamer* streamer = (__bridge AudioStreamer*)inClientData;
+  [streamer handleBufferCompleteForQueue:inAQ buffer:inBuffer];
+}
+
+/* AudioQueue callback that a property has changed, invoked on AudioQueue's own
+ * personal threads like above */
+static void MyAudioQueueIsRunningCallback(void *inUserData, AudioQueueRef inAQ,
+                                   AudioQueuePropertyID inID) {
+  AudioStreamer* streamer = (__bridge AudioStreamer *)inUserData;
+  [streamer handlePropertyChangeForQueue:inAQ propertyID:inID];
+}
+
+/* CFReadStream callback when an event has occurred */
+static void ASReadStreamCallBack(CFReadStreamRef aStream, CFStreamEventType eventType,
+                          void* inClientInfo) {
+  AudioStreamer* streamer = (__bridge AudioStreamer *)inClientInfo;
+  [streamer handleReadFromStream:aStream eventType:eventType];
+}
+
++ (AudioStreamer*) streamWithURL:(NSURL*)url{
+  assert(url != nil);
+  AudioStreamer *stream = [[AudioStreamer alloc] init];
+  stream->url = url;
+  stream->bufferCnt  = kDefaultNumAQBufs;
+  stream->bufferSize = kDefaultAQDefaultBufSize;
+  stream->timeoutInterval = 10;
+  return stream;
+}
+
+- (void)dealloc {
+  [self stop];
+  assert(queued_head == NULL);
+  assert(queued_tail == NULL);
+  assert(timeout == nil);
+  assert(buffers == NULL);
+  assert(inuse == NULL);
+}
+
+- (void) setHTTPProxy:(NSString*)host port:(int)port {
+  proxyHost = host;
+  proxyPort = port;
+  proxyType = PROXY_HTTP;
+}
+
+- (void) setSOCKSProxy:(NSString*)host port:(int)port {
+  proxyHost = host;
+  proxyPort = port;
+  proxyType = PROXY_SOCKS;
+}
+
+- (BOOL)setVolume: (double) volume {
+  if (audioQueue != NULL) {
+    AudioQueueSetParameter(audioQueue, kAudioQueueParam_Volume, volume);
+    return YES;
+  }
+  return NO;
+}
+
++ (NSString *)stringForErrorCode:(AudioStreamerErrorCode)anErrorCode {
+  switch (anErrorCode) {
+    case AS_NO_ERROR:
+      return @"No error.";
+    case AS_FILE_STREAM_GET_PROPERTY_FAILED:
+      return @"File stream get property failed";
+    case AS_FILE_STREAM_SET_PROPERTY_FAILED:
+      return @"File stream set property failed";
+    case AS_FILE_STREAM_SEEK_FAILED:
+      return @"File stream seek failed";
+    case AS_FILE_STREAM_PARSE_BYTES_FAILED:
+      return @"Parse bytes failed";
+    case AS_AUDIO_QUEUE_CREATION_FAILED:
+      return @"Audio queue creation failed";
+    case AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED:
+      return @"Audio queue buffer allocation failed";
+    case AS_AUDIO_QUEUE_ENQUEUE_FAILED:
+      return @"Queueing of audio buffer failed";
+    case AS_AUDIO_QUEUE_ADD_LISTENER_FAILED:
+      return @"Failed to add listener to audio queue";
+    case AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED:
+      return @"Failed to remove listener from audio queue";
+    case AS_AUDIO_QUEUE_START_FAILED:
+      return @"Failed to start the audio queue";
+    case AS_AUDIO_QUEUE_BUFFER_MISMATCH:
+      return @"Audio queue buffer mismatch";
+    case AS_FILE_STREAM_OPEN_FAILED:
+      return @"Failed to open file stream";
+    case AS_FILE_STREAM_CLOSE_FAILED:
+      return @"Failed to close the file stream";
+    case AS_AUDIO_QUEUE_DISPOSE_FAILED:
+      return @"Couldn't dispose of audio queue";
+    case AS_AUDIO_QUEUE_PAUSE_FAILED:
+      return @"Failed to pause the audio queue";
+    case AS_AUDIO_QUEUE_FLUSH_FAILED:
+      return @"Failed to flush the audio queue";
+    case AS_AUDIO_DATA_NOT_FOUND:
+      return @"No audio data found";
+    case AS_GET_AUDIO_TIME_FAILED:
+      return @"Couldn't get audio time";
+    case AS_NETWORK_CONNECTION_FAILED:
+      return @"Network connection failure";
+    case AS_AUDIO_QUEUE_STOP_FAILED:
+      return @"Audio queue stop failed";
+    case AS_AUDIO_STREAMER_FAILED:
+      return @"Audio streamer failed";
+    case AS_AUDIO_BUFFER_TOO_SMALL:
+      return @"Audio buffer too small";
+    default:
+      break;
+  }
+
+  return @"Audio streaming failed";
+}
+
+- (BOOL)isPlaying {
+  return state_ == AS_PLAYING;
+}
+
+- (BOOL)isPaused {
+  return state_ == AS_PAUSED;
+}
+
+- (BOOL)isWaiting {
+  return state_ == AS_WAITING_FOR_DATA ||
+         state_ == AS_WAITING_FOR_QUEUE_TO_START;
+}
+
+- (BOOL)isDone {
+  return state_ == AS_DONE || state_ == AS_STOPPED;
+}
+
+- (AudioStreamerDoneReason)doneReason {
+  if (errorCode) {
+    return AS_DONE_ERROR;
+  }
+  switch (state_) {
+    case AS_STOPPED:
+      return AS_DONE_STOPPED;
+    case AS_DONE:
+      return AS_DONE_EOF;
+    default:
+      break;
+  }
+  return AS_NOT_DONE;
+}
+
+- (BOOL) start {
+  if (stream != NULL) return NO;
+  assert(audioQueue == NULL);
+  assert(state_ == AS_INITIALIZED);
+  [self openReadStream];
+  timeout = [NSTimer scheduledTimerWithTimeInterval:timeoutInterval
+                                             target:self
+                                           selector:@selector(checkTimeout)
+                                           userInfo:nil
+                                            repeats:YES];
+  return YES;
+}
+
+- (BOOL) pause {
+  if (state_ != AS_PLAYING) return NO;
+  assert(audioQueue != NULL);
+  err = AudioQueuePause(audioQueue);
+  if (err) {
+    [self failWithErrorCode:AS_AUDIO_QUEUE_PAUSE_FAILED];
+    return NO;
+  }
+  [self setState:AS_PAUSED];
+  return YES;
+}
+
+- (BOOL) play {
+  if (state_ != AS_PAUSED) return NO;
+  assert(audioQueue != NULL);
+  err = AudioQueueStart(audioQueue, NULL);