├── .DS_Store ├── README.md ├── screenhot.png ├── sostart.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── sostart.xccheckout │ └── xcuserdata │ │ └── myoula.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── myoula.xcuserdatad │ └── xcschemes │ ├── sostart.xcscheme │ └── xcschememanagement.plist ├── sostart ├── AudioStreamer.h ├── AudioStreamer.m ├── Base.lproj │ └── MainMenu.xib ├── Images.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-256.png │ │ └── icon-32.png ├── SSAppDelegate.h ├── SSAppDelegate.m ├── en.lproj │ ├── Credits.rtf │ └── InfoPlist.strings ├── icon-128.png ├── icon-16.png ├── icon-256.png ├── icon-32.png ├── main.m ├── next-high.png ├── next.png ├── play-high.png ├── play.png ├── sostart-Info.plist ├── sostart-Prefix.pch ├── stop-high.png └── stop.png └── sostartTests ├── en.lproj └── InfoPlist.strings ├── sostartTests-Info.plist └── sostartTests.m /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sostart 2 | ======= 3 | 4 | 开听电台mac版 http://www.sostart.com/ 5 | 6 | 截图: 7 | ![alt text](https://raw.github.com/myoula/sostart/master/screenhot.png "logo") -------------------------------------------------------------------------------- /screenhot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/screenhot.png -------------------------------------------------------------------------------- /sostart.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8D394D30186139A300958966 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D394D2F186139A300958966 /* Cocoa.framework */; }; 11 | 8D394D3A186139A300958966 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8D394D38186139A300958966 /* InfoPlist.strings */; }; 12 | 8D394D3C186139A300958966 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D394D3B186139A300958966 /* main.m */; }; 13 | 8D394D40186139A400958966 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 8D394D3E186139A400958966 /* Credits.rtf */; }; 14 | 8D394D43186139A400958966 /* SSAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D394D42186139A400958966 /* SSAppDelegate.m */; }; 15 | 8D394D46186139A400958966 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8D394D44186139A400958966 /* MainMenu.xib */; }; 16 | 8D394D48186139A400958966 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D394D47186139A400958966 /* Images.xcassets */; }; 17 | 8D394D4F186139A400958966 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D394D4E186139A400958966 /* XCTest.framework */; }; 18 | 8D394D50186139A400958966 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D394D2F186139A300958966 /* Cocoa.framework */; }; 19 | 8D394D58186139A400958966 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8D394D56186139A400958966 /* InfoPlist.strings */; }; 20 | 8D394D5A186139A400958966 /* sostartTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D394D59186139A400958966 /* sostartTests.m */; }; 21 | 8DE562871861C9F700862EA7 /* AudioStreamer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DE562861861C9F700862EA7 /* AudioStreamer.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 22 | 8DE562891861CC9D00862EA7 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE562881861CC9D00862EA7 /* AudioToolbox.framework */; }; 23 | 8DFD1E9418627C5C00B817E0 /* next.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9118627C5C00B817E0 /* next.png */; }; 24 | 8DFD1E9518627C5C00B817E0 /* play.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9218627C5C00B817E0 /* play.png */; }; 25 | 8DFD1E9618627C5C00B817E0 /* stop.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9318627C5C00B817E0 /* stop.png */; }; 26 | 8DFD1E9918627E6D00B817E0 /* play-high.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9818627E6D00B817E0 /* play-high.png */; }; 27 | 8DFD1E9C1862827900B817E0 /* next-high.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9A1862827900B817E0 /* next-high.png */; }; 28 | 8DFD1E9D1862827900B817E0 /* stop-high.png in Resources */ = {isa = PBXBuildFile; fileRef = 8DFD1E9B1862827900B817E0 /* stop-high.png */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | 8D394D51186139A400958966 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 8D394D24186139A300958966 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 8D394D2B186139A300958966; 37 | remoteInfo = sostart; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 8D394D2C186139A300958966 /* sostart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sostart.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 8D394D2F186139A300958966 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 44 | 8D394D32186139A300958966 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 45 | 8D394D33186139A300958966 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 46 | 8D394D34186139A300958966 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 47 | 8D394D37186139A300958966 /* sostart-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "sostart-Info.plist"; sourceTree = ""; }; 48 | 8D394D39186139A300958966 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 49 | 8D394D3B186139A300958966 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 50 | 8D394D3D186139A300958966 /* sostart-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "sostart-Prefix.pch"; sourceTree = ""; }; 51 | 8D394D3F186139A400958966 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 52 | 8D394D41186139A400958966 /* SSAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SSAppDelegate.h; sourceTree = ""; }; 53 | 8D394D42186139A400958966 /* SSAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SSAppDelegate.m; sourceTree = ""; }; 54 | 8D394D45186139A400958966 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 55 | 8D394D47186139A400958966 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 56 | 8D394D4D186139A400958966 /* sostartTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = sostartTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 8D394D4E186139A400958966 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 58 | 8D394D55186139A400958966 /* sostartTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "sostartTests-Info.plist"; sourceTree = ""; }; 59 | 8D394D57186139A400958966 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 60 | 8D394D59186139A400958966 /* sostartTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = sostartTests.m; sourceTree = ""; }; 61 | 8DE562851861C9F700862EA7 /* AudioStreamer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioStreamer.h; sourceTree = ""; }; 62 | 8DE562861861C9F700862EA7 /* AudioStreamer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioStreamer.m; sourceTree = ""; }; 63 | 8DE562881861CC9D00862EA7 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 64 | 8DFD1E9118627C5C00B817E0 /* next.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = next.png; sourceTree = ""; }; 65 | 8DFD1E9218627C5C00B817E0 /* play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play.png; sourceTree = ""; }; 66 | 8DFD1E9318627C5C00B817E0 /* stop.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = stop.png; sourceTree = ""; }; 67 | 8DFD1E9818627E6D00B817E0 /* play-high.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play-high.png"; sourceTree = ""; }; 68 | 8DFD1E9A1862827900B817E0 /* next-high.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "next-high.png"; sourceTree = ""; }; 69 | 8DFD1E9B1862827900B817E0 /* stop-high.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "stop-high.png"; sourceTree = ""; }; 70 | /* End PBXFileReference section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | 8D394D29186139A300958966 /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | 8D394D30186139A300958966 /* Cocoa.framework in Frameworks */, 78 | 8DE562891861CC9D00862EA7 /* AudioToolbox.framework in Frameworks */, 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | 8D394D4A186139A400958966 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | 8D394D50186139A400958966 /* Cocoa.framework in Frameworks */, 87 | 8D394D4F186139A400958966 /* XCTest.framework in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | /* End PBXFrameworksBuildPhase section */ 92 | 93 | /* Begin PBXGroup section */ 94 | 8D394D23186139A300958966 = { 95 | isa = PBXGroup; 96 | children = ( 97 | 8D394D35186139A300958966 /* sostart */, 98 | 8D394D53186139A400958966 /* sostartTests */, 99 | 8D394D2E186139A300958966 /* Frameworks */, 100 | 8D394D2D186139A300958966 /* Products */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 8D394D2D186139A300958966 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 8D394D2C186139A300958966 /* sostart.app */, 108 | 8D394D4D186139A400958966 /* sostartTests.xctest */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 8D394D2E186139A300958966 /* Frameworks */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 8DE562881861CC9D00862EA7 /* AudioToolbox.framework */, 117 | 8D394D2F186139A300958966 /* Cocoa.framework */, 118 | 8D394D4E186139A400958966 /* XCTest.framework */, 119 | 8D394D31186139A300958966 /* Other Frameworks */, 120 | ); 121 | name = Frameworks; 122 | sourceTree = ""; 123 | }; 124 | 8D394D31186139A300958966 /* Other Frameworks */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 8D394D32186139A300958966 /* AppKit.framework */, 128 | 8D394D33186139A300958966 /* CoreData.framework */, 129 | 8D394D34186139A300958966 /* Foundation.framework */, 130 | ); 131 | name = "Other Frameworks"; 132 | sourceTree = ""; 133 | }; 134 | 8D394D35186139A300958966 /* sostart */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 8DFD1E9718627C6200B817E0 /* images */, 138 | 8DE562851861C9F700862EA7 /* AudioStreamer.h */, 139 | 8DE562861861C9F700862EA7 /* AudioStreamer.m */, 140 | 8D394D41186139A400958966 /* SSAppDelegate.h */, 141 | 8D394D42186139A400958966 /* SSAppDelegate.m */, 142 | 8D394D44186139A400958966 /* MainMenu.xib */, 143 | 8D394D47186139A400958966 /* Images.xcassets */, 144 | 8D394D36186139A300958966 /* Supporting Files */, 145 | ); 146 | path = sostart; 147 | sourceTree = ""; 148 | }; 149 | 8D394D36186139A300958966 /* Supporting Files */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 8D394D37186139A300958966 /* sostart-Info.plist */, 153 | 8D394D38186139A300958966 /* InfoPlist.strings */, 154 | 8D394D3B186139A300958966 /* main.m */, 155 | 8D394D3D186139A300958966 /* sostart-Prefix.pch */, 156 | 8D394D3E186139A400958966 /* Credits.rtf */, 157 | ); 158 | name = "Supporting Files"; 159 | sourceTree = ""; 160 | }; 161 | 8D394D53186139A400958966 /* sostartTests */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 8D394D59186139A400958966 /* sostartTests.m */, 165 | 8D394D54186139A400958966 /* Supporting Files */, 166 | ); 167 | path = sostartTests; 168 | sourceTree = ""; 169 | }; 170 | 8D394D54186139A400958966 /* Supporting Files */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 8D394D55186139A400958966 /* sostartTests-Info.plist */, 174 | 8D394D56186139A400958966 /* InfoPlist.strings */, 175 | ); 176 | name = "Supporting Files"; 177 | sourceTree = ""; 178 | }; 179 | 8DFD1E9718627C6200B817E0 /* images */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 8DFD1E9A1862827900B817E0 /* next-high.png */, 183 | 8DFD1E9B1862827900B817E0 /* stop-high.png */, 184 | 8DFD1E9818627E6D00B817E0 /* play-high.png */, 185 | 8DFD1E9118627C5C00B817E0 /* next.png */, 186 | 8DFD1E9218627C5C00B817E0 /* play.png */, 187 | 8DFD1E9318627C5C00B817E0 /* stop.png */, 188 | ); 189 | name = images; 190 | sourceTree = ""; 191 | }; 192 | /* End PBXGroup section */ 193 | 194 | /* Begin PBXNativeTarget section */ 195 | 8D394D2B186139A300958966 /* sostart */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = 8D394D5D186139A400958966 /* Build configuration list for PBXNativeTarget "sostart" */; 198 | buildPhases = ( 199 | 8D394D28186139A300958966 /* Sources */, 200 | 8D394D29186139A300958966 /* Frameworks */, 201 | 8D394D2A186139A300958966 /* Resources */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | ); 207 | name = sostart; 208 | productName = sostart; 209 | productReference = 8D394D2C186139A300958966 /* sostart.app */; 210 | productType = "com.apple.product-type.application"; 211 | }; 212 | 8D394D4C186139A400958966 /* sostartTests */ = { 213 | isa = PBXNativeTarget; 214 | buildConfigurationList = 8D394D60186139A400958966 /* Build configuration list for PBXNativeTarget "sostartTests" */; 215 | buildPhases = ( 216 | 8D394D49186139A400958966 /* Sources */, 217 | 8D394D4A186139A400958966 /* Frameworks */, 218 | 8D394D4B186139A400958966 /* Resources */, 219 | ); 220 | buildRules = ( 221 | ); 222 | dependencies = ( 223 | 8D394D52186139A400958966 /* PBXTargetDependency */, 224 | ); 225 | name = sostartTests; 226 | productName = sostartTests; 227 | productReference = 8D394D4D186139A400958966 /* sostartTests.xctest */; 228 | productType = "com.apple.product-type.bundle.unit-test"; 229 | }; 230 | /* End PBXNativeTarget section */ 231 | 232 | /* Begin PBXProject section */ 233 | 8D394D24186139A300958966 /* Project object */ = { 234 | isa = PBXProject; 235 | attributes = { 236 | CLASSPREFIX = SS; 237 | LastUpgradeCheck = 0500; 238 | ORGANIZATIONNAME = myoula; 239 | TargetAttributes = { 240 | 8D394D4C186139A400958966 = { 241 | TestTargetID = 8D394D2B186139A300958966; 242 | }; 243 | }; 244 | }; 245 | buildConfigurationList = 8D394D27186139A300958966 /* Build configuration list for PBXProject "sostart" */; 246 | compatibilityVersion = "Xcode 3.2"; 247 | developmentRegion = English; 248 | hasScannedForEncodings = 0; 249 | knownRegions = ( 250 | en, 251 | Base, 252 | ); 253 | mainGroup = 8D394D23186139A300958966; 254 | productRefGroup = 8D394D2D186139A300958966 /* Products */; 255 | projectDirPath = ""; 256 | projectRoot = ""; 257 | targets = ( 258 | 8D394D2B186139A300958966 /* sostart */, 259 | 8D394D4C186139A400958966 /* sostartTests */, 260 | ); 261 | }; 262 | /* End PBXProject section */ 263 | 264 | /* Begin PBXResourcesBuildPhase section */ 265 | 8D394D2A186139A300958966 /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | 8D394D3A186139A300958966 /* InfoPlist.strings in Resources */, 270 | 8DFD1E9418627C5C00B817E0 /* next.png in Resources */, 271 | 8DFD1E9C1862827900B817E0 /* next-high.png in Resources */, 272 | 8D394D48186139A400958966 /* Images.xcassets in Resources */, 273 | 8DFD1E9918627E6D00B817E0 /* play-high.png in Resources */, 274 | 8DFD1E9618627C5C00B817E0 /* stop.png in Resources */, 275 | 8D394D40186139A400958966 /* Credits.rtf in Resources */, 276 | 8D394D46186139A400958966 /* MainMenu.xib in Resources */, 277 | 8DFD1E9518627C5C00B817E0 /* play.png in Resources */, 278 | 8DFD1E9D1862827900B817E0 /* stop-high.png in Resources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | 8D394D4B186139A400958966 /* Resources */ = { 283 | isa = PBXResourcesBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | 8D394D58186139A400958966 /* InfoPlist.strings in Resources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXResourcesBuildPhase section */ 291 | 292 | /* Begin PBXSourcesBuildPhase section */ 293 | 8D394D28186139A300958966 /* Sources */ = { 294 | isa = PBXSourcesBuildPhase; 295 | buildActionMask = 2147483647; 296 | files = ( 297 | 8D394D43186139A400958966 /* SSAppDelegate.m in Sources */, 298 | 8D394D3C186139A300958966 /* main.m in Sources */, 299 | 8DE562871861C9F700862EA7 /* AudioStreamer.m in Sources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | 8D394D49186139A400958966 /* Sources */ = { 304 | isa = PBXSourcesBuildPhase; 305 | buildActionMask = 2147483647; 306 | files = ( 307 | 8D394D5A186139A400958966 /* sostartTests.m in Sources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | /* End PBXSourcesBuildPhase section */ 312 | 313 | /* Begin PBXTargetDependency section */ 314 | 8D394D52186139A400958966 /* PBXTargetDependency */ = { 315 | isa = PBXTargetDependency; 316 | target = 8D394D2B186139A300958966 /* sostart */; 317 | targetProxy = 8D394D51186139A400958966 /* PBXContainerItemProxy */; 318 | }; 319 | /* End PBXTargetDependency section */ 320 | 321 | /* Begin PBXVariantGroup section */ 322 | 8D394D38186139A300958966 /* InfoPlist.strings */ = { 323 | isa = PBXVariantGroup; 324 | children = ( 325 | 8D394D39186139A300958966 /* en */, 326 | ); 327 | name = InfoPlist.strings; 328 | sourceTree = ""; 329 | }; 330 | 8D394D3E186139A400958966 /* Credits.rtf */ = { 331 | isa = PBXVariantGroup; 332 | children = ( 333 | 8D394D3F186139A400958966 /* en */, 334 | ); 335 | name = Credits.rtf; 336 | sourceTree = ""; 337 | }; 338 | 8D394D44186139A400958966 /* MainMenu.xib */ = { 339 | isa = PBXVariantGroup; 340 | children = ( 341 | 8D394D45186139A400958966 /* Base */, 342 | ); 343 | name = MainMenu.xib; 344 | sourceTree = ""; 345 | }; 346 | 8D394D56186139A400958966 /* InfoPlist.strings */ = { 347 | isa = PBXVariantGroup; 348 | children = ( 349 | 8D394D57186139A400958966 /* en */, 350 | ); 351 | name = InfoPlist.strings; 352 | sourceTree = ""; 353 | }; 354 | /* End PBXVariantGroup section */ 355 | 356 | /* Begin XCBuildConfiguration section */ 357 | 8D394D5B186139A400958966 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_CONSTANT_CONVERSION = YES; 367 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 368 | CLANG_WARN_EMPTY_BODY = YES; 369 | CLANG_WARN_ENUM_CONVERSION = YES; 370 | CLANG_WARN_INT_CONVERSION = YES; 371 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | COPY_PHASE_STRIP = NO; 374 | GCC_C_LANGUAGE_STANDARD = gnu99; 375 | GCC_DYNAMIC_NO_PIC = NO; 376 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 377 | GCC_OPTIMIZATION_LEVEL = 0; 378 | GCC_PREPROCESSOR_DEFINITIONS = ( 379 | "DEBUG=1", 380 | "$(inherited)", 381 | ); 382 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 385 | GCC_WARN_UNDECLARED_SELECTOR = YES; 386 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 387 | GCC_WARN_UNUSED_FUNCTION = YES; 388 | GCC_WARN_UNUSED_VARIABLE = YES; 389 | MACOSX_DEPLOYMENT_TARGET = 10.8; 390 | ONLY_ACTIVE_ARCH = NO; 391 | SDKROOT = macosx10.8; 392 | }; 393 | name = Debug; 394 | }; 395 | 8D394D5C186139A400958966 /* Release */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | ALWAYS_SEARCH_USER_PATHS = NO; 399 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 401 | CLANG_CXX_LIBRARY = "libc++"; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_WARN_BOOL_CONVERSION = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INT_CONVERSION = YES; 409 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 410 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 411 | COPY_PHASE_STRIP = YES; 412 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 413 | ENABLE_NS_ASSERTIONS = NO; 414 | GCC_C_LANGUAGE_STANDARD = gnu99; 415 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 416 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 417 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 418 | GCC_WARN_UNDECLARED_SELECTOR = YES; 419 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 420 | GCC_WARN_UNUSED_FUNCTION = YES; 421 | GCC_WARN_UNUSED_VARIABLE = YES; 422 | MACOSX_DEPLOYMENT_TARGET = 10.8; 423 | ONLY_ACTIVE_ARCH = NO; 424 | SDKROOT = macosx10.8; 425 | }; 426 | name = Release; 427 | }; 428 | 8D394D5E186139A400958966 /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | COMBINE_HIDPI_IMAGES = YES; 433 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 434 | GCC_PREFIX_HEADER = "sostart/sostart-Prefix.pch"; 435 | INFOPLIST_FILE = "sostart/sostart-Info.plist"; 436 | MACOSX_DEPLOYMENT_TARGET = 10.7; 437 | PRODUCT_NAME = "$(TARGET_NAME)"; 438 | WRAPPER_EXTENSION = app; 439 | }; 440 | name = Debug; 441 | }; 442 | 8D394D5F186139A400958966 /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 446 | COMBINE_HIDPI_IMAGES = YES; 447 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 448 | GCC_PREFIX_HEADER = "sostart/sostart-Prefix.pch"; 449 | INFOPLIST_FILE = "sostart/sostart-Info.plist"; 450 | MACOSX_DEPLOYMENT_TARGET = 10.7; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | WRAPPER_EXTENSION = app; 453 | }; 454 | name = Release; 455 | }; 456 | 8D394D61186139A400958966 /* Debug */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/sostart.app/Contents/MacOS/sostart"; 460 | COMBINE_HIDPI_IMAGES = YES; 461 | FRAMEWORK_SEARCH_PATHS = ( 462 | "$(DEVELOPER_FRAMEWORKS_DIR)", 463 | "$(inherited)", 464 | ); 465 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 466 | GCC_PREFIX_HEADER = "sostart/sostart-Prefix.pch"; 467 | GCC_PREPROCESSOR_DEFINITIONS = ( 468 | "DEBUG=1", 469 | "$(inherited)", 470 | ); 471 | INFOPLIST_FILE = "sostartTests/sostartTests-Info.plist"; 472 | PRODUCT_NAME = "$(TARGET_NAME)"; 473 | TEST_HOST = "$(BUNDLE_LOADER)"; 474 | WRAPPER_EXTENSION = xctest; 475 | }; 476 | name = Debug; 477 | }; 478 | 8D394D62186139A400958966 /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/sostart.app/Contents/MacOS/sostart"; 482 | COMBINE_HIDPI_IMAGES = YES; 483 | FRAMEWORK_SEARCH_PATHS = ( 484 | "$(DEVELOPER_FRAMEWORKS_DIR)", 485 | "$(inherited)", 486 | ); 487 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 488 | GCC_PREFIX_HEADER = "sostart/sostart-Prefix.pch"; 489 | INFOPLIST_FILE = "sostartTests/sostartTests-Info.plist"; 490 | PRODUCT_NAME = "$(TARGET_NAME)"; 491 | TEST_HOST = "$(BUNDLE_LOADER)"; 492 | WRAPPER_EXTENSION = xctest; 493 | }; 494 | name = Release; 495 | }; 496 | /* End XCBuildConfiguration section */ 497 | 498 | /* Begin XCConfigurationList section */ 499 | 8D394D27186139A300958966 /* Build configuration list for PBXProject "sostart" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | 8D394D5B186139A400958966 /* Debug */, 503 | 8D394D5C186139A400958966 /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | 8D394D5D186139A400958966 /* Build configuration list for PBXNativeTarget "sostart" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 8D394D5E186139A400958966 /* Debug */, 512 | 8D394D5F186139A400958966 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | 8D394D60186139A400958966 /* Build configuration list for PBXNativeTarget "sostartTests" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | 8D394D61186139A400958966 /* Debug */, 521 | 8D394D62186139A400958966 /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | /* End XCConfigurationList section */ 527 | }; 528 | rootObject = 8D394D24186139A300958966 /* Project object */; 529 | } 530 | -------------------------------------------------------------------------------- /sostart.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sostart.xcodeproj/project.xcworkspace/xcshareddata/sostart.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 8D775257-9FE6-43B0-8D9D-4938866AF4CF 9 | IDESourceControlProjectName 10 | sostart 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 0747D517-5451-4765-A40B-F34450E91C3F 14 | https://github.com/myoula/sostart 15 | 16 | IDESourceControlProjectPath 17 | sostart.xcodeproj/project.xcworkspace 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 0747D517-5451-4765-A40B-F34450E91C3F 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/myoula/sostart 25 | IDESourceControlProjectVersion 26 | 110 27 | IDESourceControlProjectWCCIdentifier 28 | 0747D517-5451-4765-A40B-F34450E91C3F 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 0747D517-5451-4765-A40B-F34450E91C3F 36 | IDESourceControlWCCName 37 | sostart 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /sostart.xcodeproj/project.xcworkspace/xcuserdata/myoula.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart.xcodeproj/project.xcworkspace/xcuserdata/myoula.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /sostart.xcodeproj/xcuserdata/myoula.xcuserdatad/xcschemes/sostart.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /sostart.xcodeproj/xcuserdata/myoula.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | sostart.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8D394D2B186139A300958966 16 | 17 | primary 18 | 19 | 20 | 8D394D4C186139A400958966 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /sostart/AudioStreamer.h: -------------------------------------------------------------------------------- 1 | // 2 | // AudioStreamer.h 3 | // StreamingAudioPlayer 4 | // 5 | // Created by Matt Gallagher on 27/09/08. 6 | // Copyright 2008 Matt Gallagher. All rights reserved. 7 | // 8 | // This software is provided 'as-is', without any express or implied 9 | // warranty. In no event will the authors be held liable for any damages 10 | // arising from the use of this software. Permission is granted to anyone to 11 | // use this software for any purpose, including commercial applications, and to 12 | // alter it and redistribute it freely, subject to the following restrictions: 13 | // 14 | // 1. The origin of this software must not be misrepresented; you must not 15 | // claim that you wrote the original software. If you use this software 16 | // in a product, an acknowledgment in the product documentation would be 17 | // appreciated but is not required. 18 | // 2. Altered source versions must be plainly marked as such, and must not be 19 | // misrepresented as being the original software. 20 | // 3. This notice may not be removed or altered from any source 21 | // distribution. 22 | // 23 | 24 | #if TARGET_OS_IPHONE 25 | #import 26 | #else 27 | #import 28 | #endif // TARGET_OS_IPHONE 29 | 30 | #include 31 | #include 32 | 33 | #define LOG_QUEUED_BUFFERS 0 34 | 35 | #define kNumAQBufs 24 // Number of audio queue buffers we allocate. 36 | // Needs to be big enough to keep audio pipeline 37 | // busy (non-zero number of queued buffers) but 38 | // not so big that audio takes too long to begin 39 | // (kNumAQBufs * kAQBufSize of data must be 40 | // loaded before playback will start). 41 | // 42 | // Set LOG_QUEUED_BUFFERS to 1 to log how many 43 | // buffers are queued at any time -- if it drops 44 | // to zero too often, this value may need to 45 | // increase. Min 3, typical 8-24. 46 | 47 | #define kAQDefaultBufSize 2048 // Number of bytes in each audio queue buffer 48 | // Needs to be big enough to hold a packet of 49 | // audio from the audio file. If number is too 50 | // large, queuing of audio before playback starts 51 | // will take too long. 52 | // Highly compressed files can use smaller 53 | // numbers (512 or less). 2048 should hold all 54 | // but the largest packets. A buffer size error 55 | // will occur if this number is too small. 56 | 57 | #define kAQMaxPacketDescs 512 // Number of packet descriptions in our array 58 | 59 | typedef enum 60 | { 61 | AS_INITIALIZED = 0, 62 | AS_STARTING_FILE_THREAD, 63 | AS_WAITING_FOR_DATA, 64 | AS_FLUSHING_EOF, 65 | AS_WAITING_FOR_QUEUE_TO_START, 66 | AS_PLAYING, 67 | AS_BUFFERING, 68 | AS_STOPPING, 69 | AS_STOPPED, 70 | AS_PAUSED 71 | } AudioStreamerState; 72 | 73 | typedef enum 74 | { 75 | AS_NO_STOP = 0, 76 | AS_STOPPING_EOF, 77 | AS_STOPPING_USER_ACTION, 78 | AS_STOPPING_ERROR, 79 | AS_STOPPING_TEMPORARILY 80 | } AudioStreamerStopReason; 81 | 82 | typedef enum 83 | { 84 | AS_NO_ERROR = 0, 85 | AS_NETWORK_CONNECTION_FAILED, 86 | AS_FILE_STREAM_GET_PROPERTY_FAILED, 87 | AS_FILE_STREAM_SET_PROPERTY_FAILED, 88 | AS_FILE_STREAM_SEEK_FAILED, 89 | AS_FILE_STREAM_PARSE_BYTES_FAILED, 90 | AS_FILE_STREAM_OPEN_FAILED, 91 | AS_FILE_STREAM_CLOSE_FAILED, 92 | AS_AUDIO_DATA_NOT_FOUND, 93 | AS_AUDIO_QUEUE_CREATION_FAILED, 94 | AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED, 95 | AS_AUDIO_QUEUE_ENQUEUE_FAILED, 96 | AS_AUDIO_QUEUE_ADD_LISTENER_FAILED, 97 | AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED, 98 | AS_AUDIO_QUEUE_START_FAILED, 99 | AS_AUDIO_QUEUE_PAUSE_FAILED, 100 | AS_AUDIO_QUEUE_BUFFER_MISMATCH, 101 | AS_AUDIO_QUEUE_DISPOSE_FAILED, 102 | AS_AUDIO_QUEUE_STOP_FAILED, 103 | AS_AUDIO_QUEUE_FLUSH_FAILED, 104 | AS_AUDIO_STREAMER_FAILED, 105 | AS_GET_AUDIO_TIME_FAILED, 106 | AS_AUDIO_BUFFER_TOO_SMALL 107 | } AudioStreamerErrorCode; 108 | 109 | extern NSString * const ASStatusChangedNotification; 110 | 111 | @interface AudioStreamer : NSObject 112 | { 113 | NSURL *url; 114 | 115 | // 116 | // Special threading consideration: 117 | // The audioQueue property should only ever be accessed inside a 118 | // synchronized(self) block and only *after* checking that ![self isFinishing] 119 | // 120 | AudioQueueRef audioQueue; 121 | AudioFileStreamID audioFileStream; // the audio file stream parser 122 | AudioStreamBasicDescription asbd; // description of the audio 123 | NSThread *internalThread; // the thread where the download and 124 | // audio file stream parsing occurs 125 | 126 | AudioQueueBufferRef audioQueueBuffer[kNumAQBufs]; // audio queue buffers 127 | AudioStreamPacketDescription packetDescs[kAQMaxPacketDescs]; // packet descriptions for enqueuing audio 128 | unsigned int fillBufferIndex; // the index of the audioQueueBuffer that is being filled 129 | UInt32 packetBufferSize; 130 | size_t bytesFilled; // how many bytes have been filled 131 | size_t packetsFilled; // how many packets have been filled 132 | bool inuse[kNumAQBufs]; // flags to indicate that a buffer is still in use 133 | NSInteger buffersUsed; 134 | NSDictionary *httpHeaders; 135 | NSString *fileExtension; 136 | 137 | AudioStreamerState state; 138 | AudioStreamerState laststate; 139 | AudioStreamerStopReason stopReason; 140 | AudioStreamerErrorCode errorCode; 141 | OSStatus err; 142 | 143 | bool discontinuous; // flag to indicate middle of the stream 144 | 145 | pthread_mutex_t queueBuffersMutex; // a mutex to protect the inuse flags 146 | pthread_cond_t queueBufferReadyCondition; // a condition varable for handling the inuse flags 147 | 148 | CFReadStreamRef stream; 149 | NSNotificationCenter *notificationCenter; 150 | 151 | UInt32 bitRate; // Bits per second in the file 152 | NSInteger dataOffset; // Offset of the first audio packet in the stream 153 | NSInteger fileLength; // Length of the file in bytes 154 | NSInteger seekByteOffset; // Seek offset within the file in bytes 155 | UInt64 audioDataByteCount; // Used when the actual number of audio bytes in 156 | // the file is known (more accurate than assuming 157 | // the whole file is audio) 158 | 159 | UInt64 processedPacketsCount; // number of packets accumulated for bitrate estimation 160 | UInt64 processedPacketsSizeTotal; // byte size of accumulated estimation packets 161 | 162 | double seekTime; 163 | BOOL seekWasRequested; 164 | double requestedSeekTime; 165 | double sampleRate; // Sample rate of the file (used to compare with 166 | // samples played by the queue for current playback 167 | // time) 168 | double packetDuration; // sample rate times frames per packet 169 | double lastProgress; // last calculated progress point 170 | #if TARGET_OS_IPHONE 171 | BOOL pausedByInterruption; 172 | #endif 173 | } 174 | 175 | @property AudioStreamerErrorCode errorCode; 176 | @property (readonly) AudioStreamerState state; 177 | @property (readonly) double progress; 178 | @property (readonly) double duration; 179 | @property (readwrite) UInt32 bitRate; 180 | @property (readonly) NSDictionary *httpHeaders; 181 | @property (copy,readwrite) NSString *fileExtension; 182 | @property (nonatomic) BOOL shouldDisplayAlertOnError; // to control whether the alert is displayed in failWithErrorCode 183 | 184 | - (id)initWithURL:(NSURL *)aURL; 185 | - (void)start; 186 | - (void)stop; 187 | - (void)pause; 188 | - (BOOL)isPlaying; 189 | - (BOOL)isPaused; 190 | - (BOOL)isWaiting; 191 | - (BOOL)isIdle; 192 | - (BOOL)isAborted; // return YES if streaming halted due to error (AS_STOPPING + AS_STOPPING_ERROR) 193 | - (void)seekToTime:(double)newSeekTime; 194 | - (double)calculatedBitRate; 195 | 196 | @end 197 | 198 | 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /sostart/AudioStreamer.m: -------------------------------------------------------------------------------- 1 | // 2 | // AudioStreamer.m 3 | // StreamingAudioPlayer 4 | // 5 | // Created by Matt Gallagher on 27/09/08. 6 | // Copyright 2008 Matt Gallagher. All rights reserved. 7 | // 8 | // This software is provided 'as-is', without any express or implied 9 | // warranty. In no event will the authors be held liable for any damages 10 | // arising from the use of this software. Permission is granted to anyone to 11 | // use this software for any purpose, including commercial applications, and to 12 | // alter it and redistribute it freely, subject to the following restrictions: 13 | // 14 | // 1. The origin of this software must not be misrepresented; you must not 15 | // claim that you wrote the original software. If you use this software 16 | // in a product, an acknowledgment in the product documentation would be 17 | // appreciated but is not required. 18 | // 2. Altered source versions must be plainly marked as such, and must not be 19 | // misrepresented as being the original software. 20 | // 3. This notice may not be removed or altered from any source 21 | // distribution. 22 | // 23 | 24 | #import "AudioStreamer.h" 25 | #if TARGET_OS_IPHONE 26 | #import 27 | #endif 28 | 29 | #define BitRateEstimationMaxPackets 5000 30 | #define BitRateEstimationMinPackets 50 31 | 32 | NSString * const ASStatusChangedNotification = @"ASStatusChangedNotification"; 33 | NSString * const ASAudioSessionInterruptionOccuredNotification = @"ASAudioSessionInterruptionOccuredNotification"; 34 | 35 | NSString * const AS_NO_ERROR_STRING = @"No error."; 36 | NSString * const AS_FILE_STREAM_GET_PROPERTY_FAILED_STRING = @"File stream get property failed."; 37 | NSString * const AS_FILE_STREAM_SEEK_FAILED_STRING = @"File stream seek failed."; 38 | NSString * const AS_FILE_STREAM_PARSE_BYTES_FAILED_STRING = @"Parse bytes failed."; 39 | NSString * const AS_FILE_STREAM_OPEN_FAILED_STRING = @"Open audio file stream failed."; 40 | NSString * const AS_FILE_STREAM_CLOSE_FAILED_STRING = @"Close audio file stream failed."; 41 | NSString * const AS_AUDIO_QUEUE_CREATION_FAILED_STRING = @"Audio queue creation failed."; 42 | NSString * const AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED_STRING = @"Audio buffer allocation failed."; 43 | NSString * const AS_AUDIO_QUEUE_ENQUEUE_FAILED_STRING = @"Queueing of audio buffer failed."; 44 | NSString * const AS_AUDIO_QUEUE_ADD_LISTENER_FAILED_STRING = @"Audio queue add listener failed."; 45 | NSString * const AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED_STRING = @"Audio queue remove listener failed."; 46 | NSString * const AS_AUDIO_QUEUE_START_FAILED_STRING = @"Audio queue start failed."; 47 | NSString * const AS_AUDIO_QUEUE_BUFFER_MISMATCH_STRING = @"Audio queue buffers don't match."; 48 | NSString * const AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING = @"Audio queue dispose failed."; 49 | NSString * const AS_AUDIO_QUEUE_PAUSE_FAILED_STRING = @"Audio queue pause failed."; 50 | NSString * const AS_AUDIO_QUEUE_STOP_FAILED_STRING = @"Audio queue stop failed."; 51 | NSString * const AS_AUDIO_DATA_NOT_FOUND_STRING = @"No audio data found."; 52 | NSString * const AS_AUDIO_QUEUE_FLUSH_FAILED_STRING = @"Audio queue flush failed."; 53 | NSString * const AS_GET_AUDIO_TIME_FAILED_STRING = @"Audio queue get current time failed."; 54 | NSString * const AS_AUDIO_STREAMER_FAILED_STRING = @"Audio playback failed"; 55 | NSString * const AS_NETWORK_CONNECTION_FAILED_STRING = @"Network connection failed"; 56 | NSString * const AS_AUDIO_BUFFER_TOO_SMALL_STRING = @"Audio packets are larger than kAQDefaultBufSize."; 57 | 58 | @interface AudioStreamer () 59 | @property (readwrite) AudioStreamerState state; 60 | @property (readwrite) AudioStreamerState laststate; 61 | 62 | - (void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream 63 | fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID 64 | ioFlags:(UInt32 *)ioFlags; 65 | - (void)handleAudioPackets:(const void *)inInputData 66 | numberBytes:(UInt32)inNumberBytes 67 | numberPackets:(UInt32)inNumberPackets 68 | packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions; 69 | - (void)handleBufferCompleteForQueue:(AudioQueueRef)inAQ 70 | buffer:(AudioQueueBufferRef)inBuffer; 71 | - (void)handlePropertyChangeForQueue:(AudioQueueRef)inAQ 72 | propertyID:(AudioQueuePropertyID)inID; 73 | 74 | #if TARGET_OS_IPHONE 75 | - (void)handleInterruptionChangeToState:(NSNotification *)notification; 76 | #endif 77 | 78 | - (void)internalSeekToTime:(double)newSeekTime; 79 | - (void)enqueueBuffer; 80 | - (void)handleReadFromStream:(CFReadStreamRef)aStream 81 | eventType:(CFStreamEventType)eventType; 82 | 83 | @end 84 | 85 | #pragma mark Audio Callback Function Implementations 86 | 87 | // 88 | // ASPropertyListenerProc 89 | // 90 | // Receives notification when the AudioFileStream has audio packets to be 91 | // played. In response, this function creates the AudioQueue, getting it 92 | // ready to begin playback (playback won't begin until audio packets are 93 | // sent to the queue in ASEnqueueBuffer). 94 | // 95 | // This function is adapted from Apple's example in AudioFileStreamExample with 96 | // kAudioQueueProperty_IsRunning listening added. 97 | // 98 | static void ASPropertyListenerProc(void * inClientData, 99 | AudioFileStreamID inAudioFileStream, 100 | AudioFileStreamPropertyID inPropertyID, 101 | UInt32 * ioFlags) 102 | { 103 | // this is called by audio file stream when it finds property values 104 | AudioStreamer* streamer = (AudioStreamer *)inClientData; 105 | [streamer 106 | handlePropertyChangeForFileStream:inAudioFileStream 107 | fileStreamPropertyID:inPropertyID 108 | ioFlags:ioFlags]; 109 | } 110 | 111 | // 112 | // ASPacketsProc 113 | // 114 | // When the AudioStream has packets to be played, this function gets an 115 | // idle audio buffer and copies the audio packets into it. The calls to 116 | // ASEnqueueBuffer won't return until there are buffers available (or the 117 | // playback has been stopped). 118 | // 119 | // This function is adapted from Apple's example in AudioFileStreamExample with 120 | // CBR functionality added. 121 | // 122 | static void ASPacketsProc( void * inClientData, 123 | UInt32 inNumberBytes, 124 | UInt32 inNumberPackets, 125 | const void * inInputData, 126 | AudioStreamPacketDescription *inPacketDescriptions) 127 | { 128 | // this is called by audio file stream when it finds packets of audio 129 | AudioStreamer* streamer = (AudioStreamer *)inClientData; 130 | [streamer 131 | handleAudioPackets:inInputData 132 | numberBytes:inNumberBytes 133 | numberPackets:inNumberPackets 134 | packetDescriptions:inPacketDescriptions]; 135 | } 136 | 137 | // 138 | // ASAudioQueueOutputCallback 139 | // 140 | // Called from the AudioQueue when playback of specific buffers completes. This 141 | // function signals from the AudioQueue thread to the AudioStream thread that 142 | // the buffer is idle and available for copying data. 143 | // 144 | // This function is unchanged from Apple's example in AudioFileStreamExample. 145 | // 146 | static void ASAudioQueueOutputCallback(void* inClientData, 147 | AudioQueueRef inAQ, 148 | AudioQueueBufferRef inBuffer) 149 | { 150 | // this is called by the audio queue when it has finished decoding our data. 151 | // The buffer is now free to be reused. 152 | AudioStreamer* streamer = (AudioStreamer*)inClientData; 153 | [streamer handleBufferCompleteForQueue:inAQ buffer:inBuffer]; 154 | } 155 | 156 | // 157 | // ASAudioQueueIsRunningCallback 158 | // 159 | // Called from the AudioQueue when playback is started or stopped. This 160 | // information is used to toggle the observable "isPlaying" property and 161 | // set the "finished" flag. 162 | // 163 | static void ASAudioQueueIsRunningCallback(void *inUserData, AudioQueueRef inAQ, AudioQueuePropertyID inID) 164 | { 165 | AudioStreamer* streamer = (AudioStreamer *)inUserData; 166 | [streamer handlePropertyChangeForQueue:inAQ propertyID:inID]; 167 | } 168 | 169 | #if TARGET_OS_IPHONE 170 | // 171 | // ASAudioSessionInterruptionListener 172 | // 173 | // Invoked if the audio session is interrupted (like when the phone rings) 174 | // 175 | static void ASAudioSessionInterruptionListener(__unused void * inClientData, UInt32 inInterruptionState) { 176 | [[NSNotificationCenter defaultCenter] postNotificationName:ASAudioSessionInterruptionOccuredNotification object:@(inInterruptionState)]; 177 | } 178 | #endif 179 | 180 | #pragma mark CFReadStream Callback Function Implementations 181 | 182 | // 183 | // ReadStreamCallBack 184 | // 185 | // This is the callback for the CFReadStream from the network connection. This 186 | // is where all network data is passed to the AudioFileStream. 187 | // 188 | // Invoked when an error occurs, the stream ends or we have data to read. 189 | // 190 | static void ASReadStreamCallBack 191 | ( 192 | CFReadStreamRef aStream, 193 | CFStreamEventType eventType, 194 | void* inClientInfo 195 | ) 196 | { 197 | AudioStreamer* streamer = (AudioStreamer *)inClientInfo; 198 | [streamer handleReadFromStream:aStream eventType:eventType]; 199 | } 200 | 201 | @implementation AudioStreamer 202 | 203 | @synthesize errorCode; 204 | @synthesize state; 205 | @synthesize laststate; 206 | @synthesize bitRate; 207 | @synthesize httpHeaders; 208 | @synthesize fileExtension; 209 | 210 | // 211 | // initWithURL 212 | // 213 | // Init method for the object. 214 | // 215 | - (id)initWithURL:(NSURL *)aURL 216 | { 217 | self = [super init]; 218 | if (self != nil) 219 | { 220 | url = [aURL retain]; 221 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruptionChangeToState:) name:ASAudioSessionInterruptionOccuredNotification object:nil]; 222 | } 223 | return self; 224 | } 225 | 226 | // 227 | // dealloc 228 | // 229 | // Releases instance memory. 230 | // 231 | - (void)dealloc 232 | { 233 | [[NSNotificationCenter defaultCenter] removeObserver:self name:ASAudioSessionInterruptionOccuredNotification object:nil]; 234 | [self stop]; 235 | [url release]; 236 | [fileExtension release]; 237 | [super dealloc]; 238 | } 239 | 240 | // 241 | // isFinishing 242 | // 243 | // returns YES if the audio has reached a stopping condition. 244 | // 245 | - (BOOL)isFinishing 246 | { 247 | @synchronized (self) 248 | { 249 | if ((errorCode != AS_NO_ERROR && state != AS_INITIALIZED) || 250 | ((state == AS_STOPPING || state == AS_STOPPED) && 251 | stopReason != AS_STOPPING_TEMPORARILY)) 252 | { 253 | return YES; 254 | } 255 | } 256 | 257 | return NO; 258 | } 259 | 260 | // 261 | // runLoopShouldExit 262 | // 263 | // returns YES if the run loop should exit. 264 | // 265 | - (BOOL)runLoopShouldExit 266 | { 267 | @synchronized(self) 268 | { 269 | if (errorCode != AS_NO_ERROR || 270 | (state == AS_STOPPED && 271 | stopReason != AS_STOPPING_TEMPORARILY)) 272 | { 273 | return YES; 274 | } 275 | } 276 | 277 | return NO; 278 | } 279 | 280 | // 281 | // stringForErrorCode: 282 | // 283 | // Converts an error code to a string that can be localized or presented 284 | // to the user. 285 | // 286 | // Parameters: 287 | // anErrorCode - the error code to convert 288 | // 289 | // returns the string representation of the error code 290 | // 291 | + (NSString *)stringForErrorCode:(AudioStreamerErrorCode)anErrorCode 292 | { 293 | switch (anErrorCode) 294 | { 295 | case AS_NO_ERROR: 296 | return AS_NO_ERROR_STRING; 297 | case AS_FILE_STREAM_GET_PROPERTY_FAILED: 298 | return AS_FILE_STREAM_GET_PROPERTY_FAILED_STRING; 299 | case AS_FILE_STREAM_SEEK_FAILED: 300 | return AS_FILE_STREAM_SEEK_FAILED_STRING; 301 | case AS_FILE_STREAM_PARSE_BYTES_FAILED: 302 | return AS_FILE_STREAM_PARSE_BYTES_FAILED_STRING; 303 | case AS_AUDIO_QUEUE_CREATION_FAILED: 304 | return AS_AUDIO_QUEUE_CREATION_FAILED_STRING; 305 | case AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED: 306 | return AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED_STRING; 307 | case AS_AUDIO_QUEUE_ENQUEUE_FAILED: 308 | return AS_AUDIO_QUEUE_ENQUEUE_FAILED_STRING; 309 | case AS_AUDIO_QUEUE_ADD_LISTENER_FAILED: 310 | return AS_AUDIO_QUEUE_ADD_LISTENER_FAILED_STRING; 311 | case AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED: 312 | return AS_AUDIO_QUEUE_REMOVE_LISTENER_FAILED_STRING; 313 | case AS_AUDIO_QUEUE_START_FAILED: 314 | return AS_AUDIO_QUEUE_START_FAILED_STRING; 315 | case AS_AUDIO_QUEUE_BUFFER_MISMATCH: 316 | return AS_AUDIO_QUEUE_BUFFER_MISMATCH_STRING; 317 | case AS_FILE_STREAM_OPEN_FAILED: 318 | return AS_FILE_STREAM_OPEN_FAILED_STRING; 319 | case AS_FILE_STREAM_CLOSE_FAILED: 320 | return AS_FILE_STREAM_CLOSE_FAILED_STRING; 321 | case AS_AUDIO_QUEUE_DISPOSE_FAILED: 322 | return AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING; 323 | case AS_AUDIO_QUEUE_PAUSE_FAILED: 324 | return AS_AUDIO_QUEUE_DISPOSE_FAILED_STRING; 325 | case AS_AUDIO_QUEUE_FLUSH_FAILED: 326 | return AS_AUDIO_QUEUE_FLUSH_FAILED_STRING; 327 | case AS_AUDIO_DATA_NOT_FOUND: 328 | return AS_AUDIO_DATA_NOT_FOUND_STRING; 329 | case AS_GET_AUDIO_TIME_FAILED: 330 | return AS_GET_AUDIO_TIME_FAILED_STRING; 331 | case AS_NETWORK_CONNECTION_FAILED: 332 | return AS_NETWORK_CONNECTION_FAILED_STRING; 333 | case AS_AUDIO_QUEUE_STOP_FAILED: 334 | return AS_AUDIO_QUEUE_STOP_FAILED_STRING; 335 | case AS_AUDIO_STREAMER_FAILED: 336 | return AS_AUDIO_STREAMER_FAILED_STRING; 337 | case AS_AUDIO_BUFFER_TOO_SMALL: 338 | return AS_AUDIO_BUFFER_TOO_SMALL_STRING; 339 | default: 340 | return AS_AUDIO_STREAMER_FAILED_STRING; 341 | } 342 | 343 | return AS_AUDIO_STREAMER_FAILED_STRING; 344 | } 345 | 346 | // 347 | // presentAlertWithTitle:message: 348 | // 349 | // Common code for presenting error dialogs 350 | // 351 | // Parameters: 352 | // title - title for the dialog 353 | // message - main test for the dialog 354 | // 355 | - (void)presentAlertWithTitle:(NSString*)title message:(NSString*)message 356 | { 357 | #if TARGET_OS_IPHONE 358 | UIAlertView *alert = [ 359 | [[UIAlertView alloc] 360 | initWithTitle:title 361 | message:message 362 | delegate:nil 363 | cancelButtonTitle:NSLocalizedString(@"OK", @"") 364 | otherButtonTitles: nil] 365 | autorelease]; 366 | [alert 367 | performSelector:@selector(show) 368 | onThread:[NSThread mainThread] 369 | withObject:nil 370 | waitUntilDone:NO]; 371 | #else 372 | NSAlert *alert = 373 | [NSAlert 374 | alertWithMessageText:title 375 | defaultButton:NSLocalizedString(@"OK", @"") 376 | alternateButton:nil 377 | otherButton:nil 378 | informativeTextWithFormat:message]; 379 | [alert 380 | performSelector:@selector(runModal) 381 | onThread:[NSThread mainThread] 382 | withObject:nil 383 | waitUntilDone:NO]; 384 | #endif 385 | } 386 | 387 | // 388 | // failWithErrorCode: 389 | // 390 | // Sets the playback state to failed and logs the error. 391 | // 392 | // Parameters: 393 | // anErrorCode - the error condition 394 | // 395 | - (void)failWithErrorCode:(AudioStreamerErrorCode)anErrorCode 396 | { 397 | @synchronized(self) 398 | { 399 | if (errorCode != AS_NO_ERROR) 400 | { 401 | // Only set the error once. 402 | return; 403 | } 404 | 405 | errorCode = anErrorCode; 406 | 407 | if (err) 408 | { 409 | char *errChars = (char *)&err; 410 | NSLog(@"%@ err: %c%c%c%c %d\n", 411 | [AudioStreamer stringForErrorCode:anErrorCode], 412 | errChars[3], errChars[2], errChars[1], errChars[0], 413 | (int)err); 414 | } 415 | else 416 | { 417 | NSLog(@"%@", [AudioStreamer stringForErrorCode:anErrorCode]); 418 | } 419 | 420 | if (state == AS_PLAYING || 421 | state == AS_PAUSED || 422 | state == AS_BUFFERING) 423 | { 424 | self.state = AS_STOPPING; 425 | stopReason = AS_STOPPING_ERROR; 426 | AudioQueueStop(audioQueue, true); 427 | } 428 | 429 | if (self.shouldDisplayAlertOnError) 430 | [self presentAlertWithTitle:NSLocalizedStringFromTable(@"File Error", @"Errors", nil) 431 | message:NSLocalizedStringFromTable(@"Unable to configure network read stream.", @"Errors", nil)]; 432 | } 433 | } 434 | 435 | // 436 | // mainThreadStateNotification 437 | // 438 | // Method invoked on main thread to send notifications to the main thread's 439 | // notification center. 440 | // 441 | - (void)mainThreadStateNotification 442 | { 443 | NSNotification *notification = 444 | [NSNotification 445 | notificationWithName:ASStatusChangedNotification 446 | object:self]; 447 | [[NSNotificationCenter defaultCenter] 448 | postNotification:notification]; 449 | } 450 | 451 | // 452 | // state 453 | // 454 | // returns the state value. 455 | // 456 | - (AudioStreamerState)state 457 | { 458 | @synchronized(self) 459 | { 460 | return state; 461 | } 462 | } 463 | 464 | // 465 | // setState: 466 | // 467 | // Sets the state and sends a notification that the state has changed. 468 | // 469 | // This method 470 | // 471 | // Parameters: 472 | // anErrorCode - the error condition 473 | // 474 | - (void)setState:(AudioStreamerState)aStatus 475 | { 476 | @synchronized(self) 477 | { 478 | if (state != aStatus) 479 | { 480 | state = aStatus; 481 | 482 | if ([[NSThread currentThread] isEqual:[NSThread mainThread]]) 483 | { 484 | [self mainThreadStateNotification]; 485 | } 486 | else 487 | { 488 | [self 489 | performSelectorOnMainThread:@selector(mainThreadStateNotification) 490 | withObject:nil 491 | waitUntilDone:NO]; 492 | } 493 | } 494 | } 495 | } 496 | 497 | // 498 | // isPlaying 499 | // 500 | // returns YES if the audio currently playing. 501 | // 502 | - (BOOL)isPlaying 503 | { 504 | if (state == AS_PLAYING) 505 | { 506 | return YES; 507 | } 508 | 509 | return NO; 510 | } 511 | 512 | // 513 | // isPaused 514 | // 515 | // returns YES if the audio currently playing. 516 | // 517 | - (BOOL)isPaused 518 | { 519 | if (state == AS_PAUSED) 520 | { 521 | return YES; 522 | } 523 | 524 | return NO; 525 | } 526 | 527 | // 528 | // isWaiting 529 | // 530 | // returns YES if the AudioStreamer is waiting for a state transition of some 531 | // kind. 532 | // 533 | - (BOOL)isWaiting 534 | { 535 | @synchronized(self) 536 | { 537 | if ([self isFinishing] || 538 | state == AS_STARTING_FILE_THREAD|| 539 | state == AS_WAITING_FOR_DATA || 540 | state == AS_WAITING_FOR_QUEUE_TO_START || 541 | state == AS_BUFFERING) 542 | { 543 | return YES; 544 | } 545 | } 546 | 547 | return NO; 548 | } 549 | 550 | // 551 | // isIdle 552 | // 553 | // returns YES if the AudioStream is in the AS_INITIALIZED state (i.e. 554 | // isn't doing anything). 555 | // 556 | - (BOOL)isIdle 557 | { 558 | if (state == AS_INITIALIZED) 559 | { 560 | return YES; 561 | } 562 | 563 | return NO; 564 | } 565 | 566 | // 567 | // isAborted 568 | // 569 | // returns YES if the AudioStream was stopped due to some errror, handled through failWithCodeError. 570 | // 571 | - (BOOL)isAborted 572 | { 573 | if (state == AS_STOPPING && stopReason == AS_STOPPING_ERROR) 574 | { 575 | return YES; 576 | } 577 | 578 | return NO; 579 | } 580 | 581 | // 582 | // hintForFileExtension: 583 | // 584 | // Generates a first guess for the file type based on the file's extension 585 | // 586 | // Parameters: 587 | // fileExtension - the file extension 588 | // 589 | // returns a file type hint that can be passed to the AudioFileStream 590 | // 591 | + (AudioFileTypeID)hintForFileExtension:(NSString *)fileExtension 592 | { 593 | AudioFileTypeID fileTypeHint = kAudioFileAAC_ADTSType; 594 | if ([fileExtension isEqual:@"mp3"]) 595 | { 596 | fileTypeHint = kAudioFileMP3Type; 597 | } 598 | else if ([fileExtension isEqual:@"wav"]) 599 | { 600 | fileTypeHint = kAudioFileWAVEType; 601 | } 602 | else if ([fileExtension isEqual:@"aifc"]) 603 | { 604 | fileTypeHint = kAudioFileAIFCType; 605 | } 606 | else if ([fileExtension isEqual:@"aiff"]) 607 | { 608 | fileTypeHint = kAudioFileAIFFType; 609 | } 610 | else if ([fileExtension isEqual:@"m4a"]) 611 | { 612 | fileTypeHint = kAudioFileM4AType; 613 | } 614 | else if ([fileExtension isEqual:@"mp4"]) 615 | { 616 | fileTypeHint = kAudioFileMPEG4Type; 617 | } 618 | else if ([fileExtension isEqual:@"caf"]) 619 | { 620 | fileTypeHint = kAudioFileCAFType; 621 | } 622 | else if ([fileExtension isEqual:@"aac"]) 623 | { 624 | fileTypeHint = kAudioFileAAC_ADTSType; 625 | } 626 | return fileTypeHint; 627 | } 628 | 629 | // 630 | // openReadStream 631 | // 632 | // Open the audioFileStream to parse data and the fileHandle as the data 633 | // source. 634 | // 635 | - (BOOL)openReadStream 636 | { 637 | @synchronized(self) 638 | { 639 | NSAssert([[NSThread currentThread] isEqual:internalThread], 640 | @"File stream download must be started on the internalThread"); 641 | NSAssert(stream == nil, @"Download stream already initialized"); 642 | 643 | // 644 | // Create the HTTP GET request 645 | // 646 | CFHTTPMessageRef message= CFHTTPMessageCreateRequest(NULL, (CFStringRef)@"GET", (CFURLRef)url, kCFHTTPVersion1_1); 647 | 648 | NSString* ref = @"http://www.sostart.com/"; 649 | CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Referer"), (CFStringRef)ref); 650 | 651 | // 652 | // If we are creating this request to seek to a location, set the 653 | // requested byte range in the headers. 654 | // 655 | if (fileLength > 0 && seekByteOffset > 0) 656 | { 657 | CFHTTPMessageSetHeaderFieldValue(message, CFSTR("Range"), 658 | (CFStringRef)[NSString stringWithFormat:@"bytes=%ld-%ld", (long)seekByteOffset, (long)fileLength]); 659 | discontinuous = YES; 660 | } 661 | 662 | // 663 | // Create the read stream that will receive data from the HTTP request 664 | // 665 | stream = CFReadStreamCreateForHTTPRequest(NULL, message); 666 | CFRelease(message); 667 | 668 | // 669 | // Enable stream redirection 670 | // 671 | if (CFReadStreamSetProperty( 672 | stream, 673 | kCFStreamPropertyHTTPShouldAutoredirect, 674 | kCFBooleanTrue) == false) 675 | { 676 | [self failWithErrorCode:AS_FILE_STREAM_SET_PROPERTY_FAILED]; 677 | 678 | return NO; 679 | } 680 | 681 | // 682 | // Handle proxies 683 | // 684 | CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings(); 685 | CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPProxy, proxySettings); 686 | CFRelease(proxySettings); 687 | 688 | // 689 | // Handle SSL connections 690 | // 691 | if([[url scheme] isEqualToString:@"https"]) 692 | { 693 | NSDictionary *sslSettings = 694 | [NSDictionary dictionaryWithObjectsAndKeys: 695 | (NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL, kCFStreamSSLLevel, 696 | [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates, 697 | [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredRoots, 698 | [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot, 699 | [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain, 700 | [NSNull null], kCFStreamSSLPeerName, 701 | nil]; 702 | 703 | CFReadStreamSetProperty(stream, kCFStreamPropertySSLSettings, sslSettings); 704 | } 705 | 706 | // 707 | // We're now ready to receive data 708 | // 709 | self.state = AS_WAITING_FOR_DATA; 710 | 711 | // 712 | // Open the stream 713 | // 714 | if (!CFReadStreamOpen(stream)) 715 | { 716 | CFRelease(stream); 717 | 718 | [self failWithErrorCode:AS_FILE_STREAM_OPEN_FAILED]; 719 | 720 | return NO; 721 | } 722 | 723 | // 724 | // Set our callback function to receive the data 725 | // 726 | CFStreamClientContext context = {0, self, NULL, NULL, NULL}; 727 | CFReadStreamSetClient( 728 | stream, 729 | kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered, 730 | ASReadStreamCallBack, 731 | &context); 732 | CFReadStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); 733 | } 734 | 735 | return YES; 736 | } 737 | 738 | // 739 | // startInternal 740 | // 741 | // This is the start method for the AudioStream thread. This thread is created 742 | // because it will be blocked when there are no audio buffers idle (and ready 743 | // to receive audio data). 744 | // 745 | // Activity in this thread: 746 | // - Creation and cleanup of all AudioFileStream and AudioQueue objects 747 | // - Receives data from the CFReadStream 748 | // - AudioFileStream processing 749 | // - Copying of data from AudioFileStream into audio buffers 750 | // - Stopping of the thread because of end-of-file 751 | // - Stopping due to error or failure 752 | // 753 | // Activity *not* in this thread: 754 | // - AudioQueue playback and notifications (happens in AudioQueue thread) 755 | // - Actual download of NSURLConnection data (NSURLConnection's thread) 756 | // - Creation of the AudioStreamer (other, likely "main" thread) 757 | // - Invocation of -start method (other, likely "main" thread) 758 | // - User/manual invocation of -stop (other, likely "main" thread) 759 | // 760 | // This method contains bits of the "main" function from Apple's example in 761 | // AudioFileStreamExample. 762 | // 763 | - (void)startInternal 764 | { 765 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 766 | 767 | @synchronized(self) 768 | { 769 | if (state != AS_STARTING_FILE_THREAD) 770 | { 771 | if (state != AS_STOPPING && 772 | state != AS_STOPPED) 773 | { 774 | NSLog(@"### Not starting audio thread. State code is: %ld", (long)state); 775 | } 776 | self.state = AS_INITIALIZED; 777 | [pool release]; 778 | return; 779 | } 780 | 781 | #if TARGET_OS_IPHONE 782 | // 783 | // Set the audio session category so that we continue to play if the 784 | // iPhone/iPod auto-locks. 785 | // 786 | AudioSessionInitialize ( 787 | NULL, // 'NULL' to use the default (main) run loop 788 | NULL, // 'NULL' to use the default run loop mode 789 | ASAudioSessionInterruptionListener, // a reference to your interruption callback 790 | self // data to pass to your interruption listener callback 791 | ); 792 | UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback; 793 | AudioSessionSetProperty ( 794 | kAudioSessionProperty_AudioCategory, 795 | sizeof (sessionCategory), 796 | &sessionCategory 797 | ); 798 | AudioSessionSetActive(true); 799 | #endif 800 | 801 | // initialize a mutex and condition so that we can block on buffers in use. 802 | pthread_mutex_init(&queueBuffersMutex, NULL); 803 | pthread_cond_init(&queueBufferReadyCondition, NULL); 804 | 805 | if (![self openReadStream]) 806 | { 807 | goto cleanup; 808 | } 809 | } 810 | 811 | // 812 | // Process the run loop until playback is finished or failed. 813 | // 814 | BOOL isRunning = YES; 815 | do 816 | { 817 | isRunning = [[NSRunLoop currentRunLoop] 818 | runMode:NSDefaultRunLoopMode 819 | beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; 820 | 821 | @synchronized(self) { 822 | if (seekWasRequested) { 823 | [self internalSeekToTime:requestedSeekTime]; 824 | seekWasRequested = NO; 825 | } 826 | } 827 | 828 | // 829 | // If there are no queued buffers, we need to check here since the 830 | // handleBufferCompleteForQueue:buffer: should not change the state 831 | // (may not enter the synchronized section). 832 | // 833 | if (buffersUsed == 0 && self.state == AS_PLAYING) 834 | { 835 | err = AudioQueuePause(audioQueue); 836 | if (err) 837 | { 838 | [self failWithErrorCode:AS_AUDIO_QUEUE_PAUSE_FAILED]; 839 | return; 840 | } 841 | self.state = AS_BUFFERING; 842 | } 843 | } while (isRunning && ![self runLoopShouldExit]); 844 | 845 | cleanup: 846 | 847 | @synchronized(self) 848 | { 849 | // 850 | // Cleanup the read stream if it is still open 851 | // 852 | if (stream) 853 | { 854 | CFReadStreamClose(stream); 855 | CFRelease(stream); 856 | stream = nil; 857 | } 858 | 859 | // 860 | // Close the audio file strea, 861 | // 862 | if (audioFileStream) 863 | { 864 | err = AudioFileStreamClose(audioFileStream); 865 | audioFileStream = nil; 866 | if (err) 867 | { 868 | [self failWithErrorCode:AS_FILE_STREAM_CLOSE_FAILED]; 869 | } 870 | } 871 | 872 | // 873 | // Dispose of the Audio Queue 874 | // 875 | if (audioQueue) 876 | { 877 | err = AudioQueueDispose(audioQueue, true); 878 | audioQueue = nil; 879 | if (err) 880 | { 881 | [self failWithErrorCode:AS_AUDIO_QUEUE_DISPOSE_FAILED]; 882 | } 883 | } 884 | 885 | pthread_mutex_destroy(&queueBuffersMutex); 886 | pthread_cond_destroy(&queueBufferReadyCondition); 887 | 888 | #if TARGET_OS_IPHONE 889 | AudioSessionSetActive(false); 890 | #endif 891 | 892 | [httpHeaders release]; 893 | httpHeaders = nil; 894 | 895 | bytesFilled = 0; 896 | packetsFilled = 0; 897 | seekByteOffset = 0; 898 | packetBufferSize = 0; 899 | self.state = AS_INITIALIZED; 900 | 901 | [internalThread release]; 902 | internalThread = nil; 903 | } 904 | 905 | [pool release]; 906 | } 907 | 908 | // 909 | // start 910 | // 911 | // Calls startInternal in a new thread. 912 | // 913 | - (void)start 914 | { 915 | @synchronized (self) 916 | { 917 | if (state == AS_PAUSED) 918 | { 919 | [self pause]; 920 | } 921 | else if (state == AS_INITIALIZED) 922 | { 923 | NSAssert([[NSThread currentThread] isEqual:[NSThread mainThread]], 924 | @"Playback can only be started from the main thread."); 925 | notificationCenter = 926 | [[NSNotificationCenter defaultCenter] retain]; 927 | self.state = AS_STARTING_FILE_THREAD; 928 | internalThread = 929 | [[NSThread alloc] 930 | initWithTarget:self 931 | selector:@selector(startInternal) 932 | object:nil]; 933 | [internalThread start]; 934 | } 935 | } 936 | } 937 | 938 | 939 | // internalSeekToTime: 940 | // 941 | // Called from our internal runloop to reopen the stream at a seeked location 942 | // 943 | - (void)internalSeekToTime:(double)newSeekTime 944 | { 945 | if ([self calculatedBitRate] == 0.0 || fileLength <= 0) 946 | { 947 | return; 948 | } 949 | 950 | // 951 | // Calculate the byte offset for seeking 952 | // 953 | seekByteOffset = dataOffset + 954 | (newSeekTime / self.duration) * (fileLength - dataOffset); 955 | 956 | // 957 | // Attempt to leave 1 useful packet at the end of the file (although in 958 | // reality, this may still seek too far if the file has a long trailer). 959 | // 960 | if (seekByteOffset > fileLength - 2 * packetBufferSize) 961 | { 962 | seekByteOffset = fileLength - 2 * packetBufferSize; 963 | } 964 | 965 | // 966 | // Store the old time from the audio queue and the time that we're seeking 967 | // to so that we'll know the correct time progress after seeking. 968 | // 969 | seekTime = newSeekTime; 970 | 971 | // 972 | // Attempt to align the seek with a packet boundary 973 | // 974 | double calculatedBitRate = [self calculatedBitRate]; 975 | if (packetDuration > 0 && 976 | calculatedBitRate > 0) 977 | { 978 | UInt32 ioFlags = 0; 979 | SInt64 packetAlignedByteOffset; 980 | SInt64 seekPacket = floor(newSeekTime / packetDuration); 981 | err = AudioFileStreamSeek(audioFileStream, seekPacket, &packetAlignedByteOffset, &ioFlags); 982 | if (!err && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)) 983 | { 984 | seekTime -= ((seekByteOffset - dataOffset) - packetAlignedByteOffset) * 8.0 / calculatedBitRate; 985 | seekByteOffset = packetAlignedByteOffset + dataOffset; 986 | } 987 | } 988 | 989 | // 990 | // Close the current read straem 991 | // 992 | if (stream) 993 | { 994 | CFReadStreamClose(stream); 995 | CFRelease(stream); 996 | stream = nil; 997 | } 998 | 999 | // 1000 | // Stop the audio queue 1001 | // 1002 | self.state = AS_STOPPING; 1003 | stopReason = AS_STOPPING_TEMPORARILY; 1004 | err = AudioQueueStop(audioQueue, true); 1005 | if (err) 1006 | { 1007 | [self failWithErrorCode:AS_AUDIO_QUEUE_STOP_FAILED]; 1008 | return; 1009 | } 1010 | 1011 | // 1012 | // Re-open the file stream. It will request a byte-range starting at 1013 | // seekByteOffset. 1014 | // 1015 | [self openReadStream]; 1016 | } 1017 | 1018 | // 1019 | // seekToTime: 1020 | // 1021 | // Attempts to seek to the new time. Will be ignored if the bitrate or fileLength 1022 | // are unknown. 1023 | // 1024 | // Parameters: 1025 | // newTime - the time to seek to 1026 | // 1027 | - (void)seekToTime:(double)newSeekTime 1028 | { 1029 | @synchronized(self) 1030 | { 1031 | seekWasRequested = YES; 1032 | requestedSeekTime = newSeekTime; 1033 | } 1034 | } 1035 | 1036 | // 1037 | // progress 1038 | // 1039 | // returns the current playback progress. Will return zero if sampleRate has 1040 | // not yet been detected. 1041 | // 1042 | - (double)progress 1043 | { 1044 | @synchronized(self) 1045 | { 1046 | if (sampleRate > 0 && ![self isFinishing]) 1047 | { 1048 | if (state != AS_PLAYING && state != AS_PAUSED && state != AS_BUFFERING && state != AS_STOPPING) 1049 | { 1050 | return lastProgress; 1051 | } 1052 | 1053 | AudioTimeStamp queueTime; 1054 | Boolean discontinuity; 1055 | err = AudioQueueGetCurrentTime(audioQueue, NULL, &queueTime, &discontinuity); 1056 | 1057 | const OSStatus AudioQueueStopped = 0x73746F70; // 0x73746F70 is 'stop' 1058 | if (err == AudioQueueStopped) 1059 | { 1060 | return lastProgress; 1061 | } 1062 | else if (err) 1063 | { 1064 | [self failWithErrorCode:AS_GET_AUDIO_TIME_FAILED]; 1065 | } 1066 | 1067 | double progress = seekTime + queueTime.mSampleTime / sampleRate; 1068 | 1069 | if (progress < 0.0) 1070 | { 1071 | progress = 0.0; 1072 | } 1073 | 1074 | lastProgress = progress; 1075 | return progress; 1076 | } 1077 | } 1078 | 1079 | return lastProgress; 1080 | } 1081 | 1082 | // 1083 | // calculatedBitRate 1084 | // 1085 | // returns the bit rate, if known. Uses packet duration times running bits per 1086 | // packet if available, otherwise it returns the nominal bitrate. Will return 1087 | // zero if no useful option available. 1088 | // 1089 | - (double)calculatedBitRate 1090 | { 1091 | if (packetDuration && processedPacketsCount > BitRateEstimationMinPackets) 1092 | { 1093 | double averagePacketByteSize = processedPacketsSizeTotal / processedPacketsCount; 1094 | return 8.0 * averagePacketByteSize / packetDuration; 1095 | } 1096 | 1097 | if (bitRate) 1098 | { 1099 | return (double)bitRate; 1100 | } 1101 | 1102 | return 0; 1103 | } 1104 | 1105 | // 1106 | // duration 1107 | // 1108 | // Calculates the duration of available audio from the bitRate and fileLength. 1109 | // 1110 | // returns the calculated duration in seconds. 1111 | // 1112 | - (double)duration 1113 | { 1114 | double calculatedBitRate = [self calculatedBitRate]; 1115 | 1116 | if (calculatedBitRate == 0 || fileLength == 0) 1117 | { 1118 | return 0.0; 1119 | } 1120 | 1121 | return (fileLength - dataOffset) / (calculatedBitRate * 0.125); 1122 | } 1123 | 1124 | // 1125 | // pause 1126 | // 1127 | // A togglable pause function. 1128 | // 1129 | - (void)pause 1130 | { 1131 | @synchronized(self) 1132 | { 1133 | if (state == AS_PLAYING || state == AS_STOPPING) 1134 | { 1135 | err = AudioQueuePause(audioQueue); 1136 | if (err) 1137 | { 1138 | [self failWithErrorCode:AS_AUDIO_QUEUE_PAUSE_FAILED]; 1139 | return; 1140 | } 1141 | self.laststate = state; 1142 | self.state = AS_PAUSED; 1143 | } 1144 | else if (state == AS_PAUSED) 1145 | { 1146 | err = AudioQueueStart(audioQueue, NULL); 1147 | if (err) 1148 | { 1149 | [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; 1150 | return; 1151 | } 1152 | self.state = self.laststate; 1153 | } 1154 | } 1155 | } 1156 | 1157 | // 1158 | // stop 1159 | // 1160 | // This method can be called to stop downloading/playback before it completes. 1161 | // It is automatically called when an error occurs. 1162 | // 1163 | // If playback has not started before this method is called, it will toggle the 1164 | // "isPlaying" property so that it is guaranteed to transition to true and 1165 | // back to false 1166 | // 1167 | - (void)stop 1168 | { 1169 | @synchronized(self) 1170 | { 1171 | if (audioQueue && 1172 | (state == AS_PLAYING || state == AS_PAUSED || 1173 | state == AS_BUFFERING || state == AS_WAITING_FOR_QUEUE_TO_START)) 1174 | { 1175 | self.state = AS_STOPPING; 1176 | stopReason = AS_STOPPING_USER_ACTION; 1177 | err = AudioQueueStop(audioQueue, true); 1178 | if (err) 1179 | { 1180 | [self failWithErrorCode:AS_AUDIO_QUEUE_STOP_FAILED]; 1181 | return; 1182 | } 1183 | } 1184 | else if (state != AS_INITIALIZED) 1185 | { 1186 | self.state = AS_STOPPED; 1187 | stopReason = AS_STOPPING_USER_ACTION; 1188 | } 1189 | seekWasRequested = NO; 1190 | } 1191 | 1192 | while (state != AS_INITIALIZED) 1193 | { 1194 | [NSThread sleepForTimeInterval:0.1]; 1195 | } 1196 | } 1197 | 1198 | // 1199 | // handleReadFromStream:eventType: 1200 | // 1201 | // Reads data from the network file stream into the AudioFileStream 1202 | // 1203 | // Parameters: 1204 | // aStream - the network file stream 1205 | // eventType - the event which triggered this method 1206 | // 1207 | - (void)handleReadFromStream:(CFReadStreamRef)aStream 1208 | eventType:(CFStreamEventType)eventType 1209 | { 1210 | if (aStream != stream) 1211 | { 1212 | // 1213 | // Ignore messages from old streams 1214 | // 1215 | return; 1216 | } 1217 | 1218 | if (eventType == kCFStreamEventErrorOccurred) 1219 | { 1220 | [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; 1221 | } 1222 | else if (eventType == kCFStreamEventEndEncountered) 1223 | { 1224 | @synchronized(self) 1225 | { 1226 | if ([self isFinishing]) 1227 | { 1228 | return; 1229 | } 1230 | } 1231 | 1232 | // 1233 | // If there is a partially filled buffer, pass it to the AudioQueue for 1234 | // processing 1235 | // 1236 | if (bytesFilled) 1237 | { 1238 | if (self.state == AS_WAITING_FOR_DATA) 1239 | { 1240 | // 1241 | // Force audio data smaller than one whole buffer to play. 1242 | // 1243 | self.state = AS_FLUSHING_EOF; 1244 | } 1245 | [self enqueueBuffer]; 1246 | } 1247 | 1248 | @synchronized(self) 1249 | { 1250 | if (state == AS_WAITING_FOR_DATA) 1251 | { 1252 | [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; 1253 | } 1254 | 1255 | // 1256 | // We left the synchronized section to enqueue the buffer so we 1257 | // must check that we are !finished again before touching the 1258 | // audioQueue 1259 | // 1260 | else if (![self isFinishing]) 1261 | { 1262 | if (audioQueue) 1263 | { 1264 | // 1265 | // Set the progress at the end of the stream 1266 | // 1267 | err = AudioQueueFlush(audioQueue); 1268 | if (err) 1269 | { 1270 | [self failWithErrorCode:AS_AUDIO_QUEUE_FLUSH_FAILED]; 1271 | return; 1272 | } 1273 | 1274 | self.state = AS_STOPPING; 1275 | stopReason = AS_STOPPING_EOF; 1276 | err = AudioQueueStop(audioQueue, false); 1277 | if (err) 1278 | { 1279 | [self failWithErrorCode:AS_AUDIO_QUEUE_FLUSH_FAILED]; 1280 | return; 1281 | } 1282 | } 1283 | else 1284 | { 1285 | self.state = AS_STOPPED; 1286 | stopReason = AS_STOPPING_EOF; 1287 | } 1288 | } 1289 | } 1290 | } 1291 | else if (eventType == kCFStreamEventHasBytesAvailable) 1292 | { 1293 | if (!httpHeaders) 1294 | { 1295 | CFTypeRef message = 1296 | CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); 1297 | httpHeaders = 1298 | (NSDictionary *)CFHTTPMessageCopyAllHeaderFields((CFHTTPMessageRef)message); 1299 | CFRelease(message); 1300 | 1301 | // 1302 | // Only read the content length if we seeked to time zero, otherwise 1303 | // we only have a subset of the total bytes. 1304 | // 1305 | if (seekByteOffset == 0) 1306 | { 1307 | fileLength = [[httpHeaders objectForKey:@"Content-Length"] integerValue]; 1308 | } 1309 | } 1310 | 1311 | if (!audioFileStream) 1312 | { 1313 | // 1314 | // Attempt to guess the file type from the URL. Reading the MIME type 1315 | // from the httpHeaders might be a better approach since lots of 1316 | // URL's don't have the right extension. 1317 | // 1318 | // If you have a fixed file-type, you may want to hardcode this. 1319 | // 1320 | if (!self.fileExtension) 1321 | { 1322 | self.fileExtension = [[url path] pathExtension]; 1323 | } 1324 | AudioFileTypeID fileTypeHint = 1325 | [AudioStreamer hintForFileExtension:self.fileExtension]; 1326 | 1327 | // create an audio file stream parser 1328 | err = AudioFileStreamOpen(self, ASPropertyListenerProc, ASPacketsProc, 1329 | fileTypeHint, &audioFileStream); 1330 | if (err) 1331 | { 1332 | [self failWithErrorCode:AS_FILE_STREAM_OPEN_FAILED]; 1333 | return; 1334 | } 1335 | } 1336 | 1337 | UInt8 bytes[kAQDefaultBufSize]; 1338 | CFIndex length; 1339 | @synchronized(self) 1340 | { 1341 | if ([self isFinishing] || !CFReadStreamHasBytesAvailable(stream)) 1342 | { 1343 | return; 1344 | } 1345 | 1346 | // 1347 | // Read the bytes from the stream 1348 | // 1349 | length = CFReadStreamRead(stream, bytes, kAQDefaultBufSize); 1350 | 1351 | if (length == -1) 1352 | { 1353 | [self failWithErrorCode:AS_AUDIO_DATA_NOT_FOUND]; 1354 | return; 1355 | } 1356 | 1357 | if (length == 0) 1358 | { 1359 | return; 1360 | } 1361 | } 1362 | 1363 | if (discontinuous) 1364 | { 1365 | err = AudioFileStreamParseBytes(audioFileStream, length, bytes, kAudioFileStreamParseFlag_Discontinuity); 1366 | if (err) 1367 | { 1368 | [self failWithErrorCode:AS_FILE_STREAM_PARSE_BYTES_FAILED]; 1369 | return; 1370 | } 1371 | } 1372 | else 1373 | { 1374 | err = AudioFileStreamParseBytes(audioFileStream, length, bytes, 0); 1375 | if (err) 1376 | { 1377 | [self failWithErrorCode:AS_FILE_STREAM_PARSE_BYTES_FAILED]; 1378 | return; 1379 | } 1380 | } 1381 | } 1382 | } 1383 | 1384 | // 1385 | // enqueueBuffer 1386 | // 1387 | // Called from ASPacketsProc and connectionDidFinishLoading to pass filled audio 1388 | // bufffers (filled by ASPacketsProc) to the AudioQueue for playback. This 1389 | // function does not return until a buffer is idle for further filling or 1390 | // the AudioQueue is stopped. 1391 | // 1392 | // This function is adapted from Apple's example in AudioFileStreamExample with 1393 | // CBR functionality added. 1394 | // 1395 | - (void)enqueueBuffer 1396 | { 1397 | @synchronized(self) 1398 | { 1399 | if ([self isFinishing] || stream == 0) 1400 | { 1401 | return; 1402 | } 1403 | 1404 | inuse[fillBufferIndex] = true; // set in use flag 1405 | buffersUsed++; 1406 | 1407 | // enqueue buffer 1408 | AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; 1409 | fillBuf->mAudioDataByteSize = bytesFilled; 1410 | 1411 | if (packetsFilled) 1412 | { 1413 | err = AudioQueueEnqueueBuffer(audioQueue, fillBuf, packetsFilled, packetDescs); 1414 | } 1415 | else 1416 | { 1417 | err = AudioQueueEnqueueBuffer(audioQueue, fillBuf, 0, NULL); 1418 | } 1419 | 1420 | if (err) 1421 | { 1422 | [self failWithErrorCode:AS_AUDIO_QUEUE_ENQUEUE_FAILED]; 1423 | return; 1424 | } 1425 | 1426 | 1427 | if (state == AS_BUFFERING || 1428 | state == AS_WAITING_FOR_DATA || 1429 | state == AS_FLUSHING_EOF || 1430 | (state == AS_STOPPED && stopReason == AS_STOPPING_TEMPORARILY)) 1431 | { 1432 | // 1433 | // Fill all the buffers before starting. This ensures that the 1434 | // AudioFileStream stays a small amount ahead of the AudioQueue to 1435 | // avoid an audio glitch playing streaming files on iPhone SDKs < 3.0 1436 | // 1437 | if (state == AS_FLUSHING_EOF || buffersUsed == kNumAQBufs - 1) 1438 | { 1439 | if (self.state == AS_BUFFERING) 1440 | { 1441 | err = AudioQueueStart(audioQueue, NULL); 1442 | if (err) 1443 | { 1444 | [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; 1445 | return; 1446 | } 1447 | self.state = AS_PLAYING; 1448 | } 1449 | else 1450 | { 1451 | self.state = AS_WAITING_FOR_QUEUE_TO_START; 1452 | 1453 | err = AudioQueueStart(audioQueue, NULL); 1454 | if (err) 1455 | { 1456 | [self failWithErrorCode:AS_AUDIO_QUEUE_START_FAILED]; 1457 | return; 1458 | } 1459 | } 1460 | } 1461 | } 1462 | 1463 | // go to next buffer 1464 | if (++fillBufferIndex >= kNumAQBufs) fillBufferIndex = 0; 1465 | bytesFilled = 0; // reset bytes filled 1466 | packetsFilled = 0; // reset packets filled 1467 | } 1468 | 1469 | // wait until next buffer is not in use 1470 | pthread_mutex_lock(&queueBuffersMutex); 1471 | while (inuse[fillBufferIndex]) 1472 | { 1473 | pthread_cond_wait(&queueBufferReadyCondition, &queueBuffersMutex); 1474 | } 1475 | pthread_mutex_unlock(&queueBuffersMutex); 1476 | } 1477 | 1478 | // 1479 | // createQueue 1480 | // 1481 | // Method to create the AudioQueue from the parameters gathered by the 1482 | // AudioFileStream. 1483 | // 1484 | // Creation is deferred to the handling of the first audio packet (although 1485 | // it could be handled any time after kAudioFileStreamProperty_ReadyToProducePackets 1486 | // is true). 1487 | // 1488 | - (void)createQueue 1489 | { 1490 | sampleRate = asbd.mSampleRate; 1491 | packetDuration = asbd.mFramesPerPacket / sampleRate; 1492 | 1493 | // create the audio queue 1494 | err = AudioQueueNewOutput(&asbd, ASAudioQueueOutputCallback, self, NULL, NULL, 0, &audioQueue); 1495 | if (err) 1496 | { 1497 | [self failWithErrorCode:AS_AUDIO_QUEUE_CREATION_FAILED]; 1498 | return; 1499 | } 1500 | 1501 | // start the queue if it has not been started already 1502 | // listen to the "isRunning" property 1503 | err = AudioQueueAddPropertyListener(audioQueue, kAudioQueueProperty_IsRunning, ASAudioQueueIsRunningCallback, self); 1504 | if (err) 1505 | { 1506 | [self failWithErrorCode:AS_AUDIO_QUEUE_ADD_LISTENER_FAILED]; 1507 | return; 1508 | } 1509 | 1510 | // get the packet size if it is available 1511 | UInt32 sizeOfUInt32 = sizeof(UInt32); 1512 | err = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_PacketSizeUpperBound, &sizeOfUInt32, &packetBufferSize); 1513 | if (err || packetBufferSize == 0) 1514 | { 1515 | err = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MaximumPacketSize, &sizeOfUInt32, &packetBufferSize); 1516 | if (err || packetBufferSize == 0) 1517 | { 1518 | // No packet size available, just use the default 1519 | packetBufferSize = kAQDefaultBufSize; 1520 | } 1521 | } 1522 | 1523 | // allocate audio queue buffers 1524 | for (unsigned int i = 0; i < kNumAQBufs; ++i) 1525 | { 1526 | err = AudioQueueAllocateBuffer(audioQueue, packetBufferSize, &audioQueueBuffer[i]); 1527 | if (err) 1528 | { 1529 | [self failWithErrorCode:AS_AUDIO_QUEUE_BUFFER_ALLOCATION_FAILED]; 1530 | return; 1531 | } 1532 | } 1533 | 1534 | // get the cookie size 1535 | UInt32 cookieSize; 1536 | Boolean writable; 1537 | OSStatus ignorableError; 1538 | ignorableError = AudioFileStreamGetPropertyInfo(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &writable); 1539 | if (ignorableError) 1540 | { 1541 | return; 1542 | } 1543 | 1544 | // get the cookie data 1545 | void* cookieData = calloc(1, cookieSize); 1546 | ignorableError = AudioFileStreamGetProperty(audioFileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, cookieData); 1547 | if (ignorableError) 1548 | { 1549 | return; 1550 | } 1551 | 1552 | // set the cookie on the queue. 1553 | ignorableError = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_MagicCookie, cookieData, cookieSize); 1554 | free(cookieData); 1555 | if (ignorableError) 1556 | { 1557 | return; 1558 | } 1559 | } 1560 | 1561 | // 1562 | // handlePropertyChangeForFileStream:fileStreamPropertyID:ioFlags: 1563 | // 1564 | // Object method which handles implementation of ASPropertyListenerProc 1565 | // 1566 | // Parameters: 1567 | // inAudioFileStream - should be the same as self->audioFileStream 1568 | // inPropertyID - the property that changed 1569 | // ioFlags - the ioFlags passed in 1570 | // 1571 | - (void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream 1572 | fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID 1573 | ioFlags:(UInt32 *)ioFlags 1574 | { 1575 | @synchronized(self) 1576 | { 1577 | if ([self isFinishing]) 1578 | { 1579 | return; 1580 | } 1581 | 1582 | if (inPropertyID == kAudioFileStreamProperty_ReadyToProducePackets) 1583 | { 1584 | discontinuous = true; 1585 | } 1586 | else if (inPropertyID == kAudioFileStreamProperty_DataOffset) 1587 | { 1588 | SInt64 offset; 1589 | UInt32 offsetSize = sizeof(offset); 1590 | err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &offset); 1591 | if (err) 1592 | { 1593 | [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; 1594 | return; 1595 | } 1596 | dataOffset = offset; 1597 | 1598 | if (audioDataByteCount) 1599 | { 1600 | fileLength = dataOffset + audioDataByteCount; 1601 | } 1602 | } 1603 | else if (inPropertyID == kAudioFileStreamProperty_AudioDataByteCount) 1604 | { 1605 | UInt32 byteCountSize = sizeof(UInt64); 1606 | err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount); 1607 | if (err) 1608 | { 1609 | [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; 1610 | return; 1611 | } 1612 | fileLength = dataOffset + audioDataByteCount; 1613 | } 1614 | else if (inPropertyID == kAudioFileStreamProperty_DataFormat) 1615 | { 1616 | if (asbd.mSampleRate == 0) 1617 | { 1618 | UInt32 asbdSize = sizeof(asbd); 1619 | 1620 | // get the stream format. 1621 | err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd); 1622 | if (err) 1623 | { 1624 | [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; 1625 | return; 1626 | } 1627 | } 1628 | } 1629 | else if (inPropertyID == kAudioFileStreamProperty_FormatList) 1630 | { 1631 | Boolean outWriteable; 1632 | UInt32 formatListSize; 1633 | err = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable); 1634 | if (err) 1635 | { 1636 | [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; 1637 | return; 1638 | } 1639 | 1640 | AudioFormatListItem *formatList = malloc(formatListSize); 1641 | err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList); 1642 | if (err) 1643 | { 1644 | free(formatList); 1645 | [self failWithErrorCode:AS_FILE_STREAM_GET_PROPERTY_FAILED]; 1646 | return; 1647 | } 1648 | 1649 | for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)) 1650 | { 1651 | AudioStreamBasicDescription pasbd = formatList[i].mASBD; 1652 | 1653 | if (pasbd.mFormatID == kAudioFormatMPEG4AAC_HE || 1654 | pasbd.mFormatID == kAudioFormatMPEG4AAC_HE_V2) 1655 | { 1656 | // 1657 | // We've found HE-AAC, remember this to tell the audio queue 1658 | // when we construct it. 1659 | // 1660 | #if !TARGET_IPHONE_SIMULATOR 1661 | asbd = pasbd; 1662 | #endif 1663 | break; 1664 | } 1665 | } 1666 | free(formatList); 1667 | } 1668 | else 1669 | { 1670 | // NSLog(@"Property is %c%c%c%c", 1671 | // ((char *)&inPropertyID)[3], 1672 | // ((char *)&inPropertyID)[2], 1673 | // ((char *)&inPropertyID)[1], 1674 | // ((char *)&inPropertyID)[0]); 1675 | } 1676 | } 1677 | } 1678 | 1679 | // 1680 | // handleAudioPackets:numberBytes:numberPackets:packetDescriptions: 1681 | // 1682 | // Object method which handles the implementation of ASPacketsProc 1683 | // 1684 | // Parameters: 1685 | // inInputData - the packet data 1686 | // inNumberBytes - byte size of the data 1687 | // inNumberPackets - number of packets in the data 1688 | // inPacketDescriptions - packet descriptions 1689 | // 1690 | - (void)handleAudioPackets:(const void *)inInputData 1691 | numberBytes:(UInt32)inNumberBytes 1692 | numberPackets:(UInt32)inNumberPackets 1693 | packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions; 1694 | { 1695 | @synchronized(self) 1696 | { 1697 | if ([self isFinishing]) 1698 | { 1699 | return; 1700 | } 1701 | 1702 | if (bitRate == 0) 1703 | { 1704 | // 1705 | // m4a and a few other formats refuse to parse the bitrate so 1706 | // we need to set an "unparseable" condition here. If you know 1707 | // the bitrate (parsed it another way) you can set it on the 1708 | // class if needed. 1709 | // 1710 | bitRate = ~0; 1711 | } 1712 | 1713 | // we have successfully read the first packests from the audio stream, so 1714 | // clear the "discontinuous" flag 1715 | if (discontinuous) 1716 | { 1717 | discontinuous = false; 1718 | } 1719 | 1720 | if (!audioQueue) 1721 | { 1722 | [self createQueue]; 1723 | } 1724 | } 1725 | 1726 | // the following code assumes we're streaming VBR data. for CBR data, the second branch is used. 1727 | if (inPacketDescriptions) 1728 | { 1729 | for (int i = 0; i < inNumberPackets; ++i) 1730 | { 1731 | SInt64 packetOffset = inPacketDescriptions[i].mStartOffset; 1732 | SInt64 packetSize = inPacketDescriptions[i].mDataByteSize; 1733 | size_t bufSpaceRemaining; 1734 | 1735 | if (processedPacketsCount < BitRateEstimationMaxPackets) 1736 | { 1737 | processedPacketsSizeTotal += packetSize; 1738 | processedPacketsCount += 1; 1739 | } 1740 | 1741 | @synchronized(self) 1742 | { 1743 | // If the audio was terminated before this point, then 1744 | // exit. 1745 | if ([self isFinishing]) 1746 | { 1747 | return; 1748 | } 1749 | 1750 | if (packetSize > packetBufferSize) 1751 | { 1752 | [self failWithErrorCode:AS_AUDIO_BUFFER_TOO_SMALL]; 1753 | } 1754 | 1755 | bufSpaceRemaining = packetBufferSize - bytesFilled; 1756 | } 1757 | 1758 | // if the space remaining in the buffer is not enough for this packet, then enqueue the buffer. 1759 | if (bufSpaceRemaining < packetSize) 1760 | { 1761 | [self enqueueBuffer]; 1762 | } 1763 | 1764 | @synchronized(self) 1765 | { 1766 | // If the audio was terminated while waiting for a buffer, then 1767 | // exit. 1768 | if ([self isFinishing]) 1769 | { 1770 | return; 1771 | } 1772 | 1773 | // 1774 | // If there was some kind of issue with enqueueBuffer and we didn't 1775 | // make space for the new audio data then back out 1776 | // 1777 | if (bytesFilled + packetSize > packetBufferSize) 1778 | { 1779 | return; 1780 | } 1781 | 1782 | // copy data to the audio queue buffer 1783 | AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; 1784 | memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)inInputData + packetOffset, packetSize); 1785 | 1786 | // fill out packet description 1787 | packetDescs[packetsFilled] = inPacketDescriptions[i]; 1788 | packetDescs[packetsFilled].mStartOffset = bytesFilled; 1789 | // keep track of bytes filled and packets filled 1790 | bytesFilled += packetSize; 1791 | packetsFilled += 1; 1792 | } 1793 | 1794 | // if that was the last free packet description, then enqueue the buffer. 1795 | size_t packetsDescsRemaining = kAQMaxPacketDescs - packetsFilled; 1796 | if (packetsDescsRemaining == 0) { 1797 | [self enqueueBuffer]; 1798 | } 1799 | } 1800 | } 1801 | else 1802 | { 1803 | size_t offset = 0; 1804 | while (inNumberBytes) 1805 | { 1806 | // if the space remaining in the buffer is not enough for this packet, then enqueue the buffer. 1807 | size_t bufSpaceRemaining = kAQDefaultBufSize - bytesFilled; 1808 | if (bufSpaceRemaining < inNumberBytes) 1809 | { 1810 | [self enqueueBuffer]; 1811 | } 1812 | 1813 | @synchronized(self) 1814 | { 1815 | // If the audio was terminated while waiting for a buffer, then 1816 | // exit. 1817 | if ([self isFinishing]) 1818 | { 1819 | return; 1820 | } 1821 | 1822 | bufSpaceRemaining = kAQDefaultBufSize - bytesFilled; 1823 | size_t copySize; 1824 | if (bufSpaceRemaining < inNumberBytes) 1825 | { 1826 | copySize = bufSpaceRemaining; 1827 | } 1828 | else 1829 | { 1830 | copySize = inNumberBytes; 1831 | } 1832 | 1833 | // 1834 | // If there was some kind of issue with enqueueBuffer and we didn't 1835 | // make space for the new audio data then back out 1836 | // 1837 | if (bytesFilled > packetBufferSize) 1838 | { 1839 | return; 1840 | } 1841 | 1842 | // copy data to the audio queue buffer 1843 | AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; 1844 | memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)(inInputData + offset), copySize); 1845 | 1846 | 1847 | // keep track of bytes filled and packets filled 1848 | bytesFilled += copySize; 1849 | packetsFilled = 0; 1850 | inNumberBytes -= copySize; 1851 | offset += copySize; 1852 | } 1853 | } 1854 | } 1855 | } 1856 | 1857 | // 1858 | // handleBufferCompleteForQueue:buffer: 1859 | // 1860 | // Handles the buffer completetion notification from the audio queue 1861 | // 1862 | // Parameters: 1863 | // inAQ - the queue 1864 | // inBuffer - the buffer 1865 | // 1866 | - (void)handleBufferCompleteForQueue:(AudioQueueRef)inAQ 1867 | buffer:(AudioQueueBufferRef)inBuffer 1868 | { 1869 | unsigned int bufIndex = -1; 1870 | for (unsigned int i = 0; i < kNumAQBufs; ++i) 1871 | { 1872 | if (inBuffer == audioQueueBuffer[i]) 1873 | { 1874 | bufIndex = i; 1875 | break; 1876 | } 1877 | } 1878 | 1879 | if (bufIndex == -1) 1880 | { 1881 | [self failWithErrorCode:AS_AUDIO_QUEUE_BUFFER_MISMATCH]; 1882 | pthread_mutex_lock(&queueBuffersMutex); 1883 | pthread_cond_signal(&queueBufferReadyCondition); 1884 | pthread_mutex_unlock(&queueBuffersMutex); 1885 | return; 1886 | } 1887 | 1888 | // signal waiting thread that the buffer is free. 1889 | pthread_mutex_lock(&queueBuffersMutex); 1890 | inuse[bufIndex] = false; 1891 | buffersUsed--; 1892 | 1893 | // 1894 | // Enable this logging to measure how many buffers are queued at any time. 1895 | // 1896 | #if LOG_QUEUED_BUFFERS 1897 | NSLog(@"Queued buffers: %ld", buffersUsed); 1898 | #endif 1899 | 1900 | pthread_cond_signal(&queueBufferReadyCondition); 1901 | pthread_mutex_unlock(&queueBuffersMutex); 1902 | } 1903 | 1904 | - (void)handlePropertyChange:(NSNumber *)num 1905 | { 1906 | [self handlePropertyChangeForQueue:NULL propertyID:[num intValue]]; 1907 | } 1908 | 1909 | // 1910 | // handlePropertyChangeForQueue:propertyID: 1911 | // 1912 | // Implementation for ASAudioQueueIsRunningCallback 1913 | // 1914 | // Parameters: 1915 | // inAQ - the audio queue 1916 | // inID - the property ID 1917 | // 1918 | - (void)handlePropertyChangeForQueue:(AudioQueueRef)inAQ 1919 | propertyID:(AudioQueuePropertyID)inID 1920 | { 1921 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 1922 | 1923 | if (![[NSThread currentThread] isEqual:internalThread]) 1924 | { 1925 | [self 1926 | performSelector:@selector(handlePropertyChange:) 1927 | onThread:internalThread 1928 | withObject:[NSNumber numberWithInt:inID] 1929 | waitUntilDone:NO 1930 | modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]; 1931 | return; 1932 | } 1933 | @synchronized(self) 1934 | { 1935 | if (inID == kAudioQueueProperty_IsRunning) 1936 | { 1937 | if (state == AS_STOPPING) 1938 | { 1939 | // Should check value of isRunning to ensure this kAudioQueueProperty_IsRunning isn't 1940 | // the *start* of a very short stream 1941 | UInt32 isRunning = 0; 1942 | UInt32 size = sizeof(UInt32); 1943 | AudioQueueGetProperty(audioQueue, inID, &isRunning, &size); 1944 | if (isRunning == 0) 1945 | { 1946 | self.state = AS_STOPPED; 1947 | } 1948 | } 1949 | else if (state == AS_WAITING_FOR_QUEUE_TO_START) 1950 | { 1951 | // 1952 | // Note about this bug avoidance quirk: 1953 | // 1954 | // On cleanup of the AudioQueue thread, on rare occasions, there would 1955 | // be a crash in CFSetContainsValue as a CFRunLoopObserver was getting 1956 | // removed from the CFRunLoop. 1957 | // 1958 | // After lots of testing, it appeared that the audio thread was 1959 | // attempting to remove CFRunLoop observers from the CFRunLoop after the 1960 | // thread had already deallocated the run loop. 1961 | // 1962 | // By creating an NSRunLoop for the AudioQueue thread, it changes the 1963 | // thread destruction order and seems to avoid this crash bug -- or 1964 | // at least I haven't had it since (nasty hard to reproduce error!) 1965 | // 1966 | [NSRunLoop currentRunLoop]; 1967 | 1968 | self.state = AS_PLAYING; 1969 | } 1970 | else 1971 | { 1972 | NSLog(@"AudioQueue changed state in unexpected way."); 1973 | } 1974 | } 1975 | } 1976 | 1977 | [pool release]; 1978 | } 1979 | 1980 | #if TARGET_OS_IPHONE 1981 | // 1982 | // handleInterruptionChangeForQueue:propertyID: 1983 | // 1984 | // Implementation for ASAudioQueueInterruptionListener 1985 | // 1986 | // Parameters: 1987 | // inAQ - the audio queue 1988 | // inID - the property ID 1989 | // 1990 | - (void)handleInterruptionChangeToState:(NSNotification *)notification { 1991 | AudioQueuePropertyID inInterruptionState = (AudioQueuePropertyID) [notification.object unsignedIntValue]; 1992 | if (inInterruptionState == kAudioSessionBeginInterruption) 1993 | { 1994 | if ([self isPlaying]) { 1995 | [self pause]; 1996 | 1997 | pausedByInterruption = YES; 1998 | } 1999 | } 2000 | else if (inInterruptionState == kAudioSessionEndInterruption) 2001 | { 2002 | AudioSessionSetActive( true ); 2003 | 2004 | if ([self isPaused] && pausedByInterruption) { 2005 | [self pause]; // this is actually resume 2006 | 2007 | pausedByInterruption = NO; // this is redundant 2008 | } 2009 | } 2010 | } 2011 | #endif 2012 | 2013 | @end 2014 | 2015 | 2016 | -------------------------------------------------------------------------------- /sostart/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | Default 523 | 524 | 525 | 526 | 527 | 528 | 529 | Left to Right 530 | 531 | 532 | 533 | 534 | 535 | 536 | Right to Left 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | Default 548 | 549 | 550 | 551 | 552 | 553 | 554 | Left to Right 555 | 556 | 557 | 558 | 559 | 560 | 561 | Right to Left 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 697 | 708 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | -------------------------------------------------------------------------------- /sostart/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "idiom" : "mac", 11 | "size" : "16x16", 12 | "scale" : "2x" 13 | }, 14 | { 15 | "size" : "32x32", 16 | "idiom" : "mac", 17 | "filename" : "icon-32.png", 18 | "scale" : "1x" 19 | }, 20 | { 21 | "idiom" : "mac", 22 | "size" : "32x32", 23 | "scale" : "2x" 24 | }, 25 | { 26 | "size" : "128x128", 27 | "idiom" : "mac", 28 | "filename" : "icon-128.png", 29 | "scale" : "1x" 30 | }, 31 | { 32 | "idiom" : "mac", 33 | "size" : "128x128", 34 | "scale" : "2x" 35 | }, 36 | { 37 | "size" : "256x256", 38 | "idiom" : "mac", 39 | "filename" : "icon-256.png", 40 | "scale" : "1x" 41 | }, 42 | { 43 | "idiom" : "mac", 44 | "size" : "256x256", 45 | "scale" : "2x" 46 | }, 47 | { 48 | "idiom" : "mac", 49 | "size" : "512x512", 50 | "scale" : "1x" 51 | }, 52 | { 53 | "idiom" : "mac", 54 | "size" : "512x512", 55 | "scale" : "2x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /sostart/Images.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/Images.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /sostart/Images.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/Images.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /sostart/Images.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/Images.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /sostart/Images.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/Images.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /sostart/SSAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SSAppDelegate.h 3 | // sostart 4 | // 5 | // Created by myoula on 13-12-18. 6 | // Copyright (c) 2013年 myoula. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SSAppDelegate : NSObject 12 | 13 | @property (assign) IBOutlet NSWindow *window; 14 | - (IBAction)play:(NSButton *)sender; 15 | - (IBAction)next:(NSButton *)sender; 16 | - (IBAction)help:(NSButton *)sender; 17 | 18 | 19 | @property (weak) IBOutlet NSButton *playbtn; 20 | @property (weak) IBOutlet NSImageView *cover; 21 | @property (weak) IBOutlet NSTextField *title; 22 | @property (weak) IBOutlet NSTextField *artist; 23 | @property (weak) IBOutlet NSTextField *playtime; 24 | @property (weak) IBOutlet NSProgressIndicator *progressbar; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /sostart/SSAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SSAppDelegate.m 3 | // sostart 4 | // 5 | // Created by myoula on 13-12-18. 6 | // Copyright (c) 2013年 myoula. All rights reserved. 7 | // 8 | 9 | #import "SSAppDelegate.h" 10 | #import "AudioStreamer.h" 11 | 12 | @implementation SSAppDelegate { 13 | NSMutableArray* musics; 14 | AudioStreamer* streamer; 15 | NSTimer* progressUpdateTimer; 16 | NSURLConnection* aSynConnection; 17 | NSMutableData* buf; 18 | NSImage* playimg; 19 | NSImage* playhighimg; 20 | NSImage* stopimg; 21 | NSImage* stophighimg; 22 | BOOL isok; 23 | } 24 | 25 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 26 | { 27 | aSynConnection = [NSURLConnection alloc]; 28 | buf = [NSMutableData alloc]; 29 | isok = FALSE; 30 | 31 | playimg = [NSImage imageNamed:@"play"]; 32 | playhighimg = [NSImage imageNamed:@"play-high"]; 33 | stopimg = [NSImage imageNamed:@"stop"]; 34 | stophighimg = [NSImage imageNamed:@"stop-high"]; 35 | musics = [[NSMutableArray alloc] init]; 36 | [self getmusiclist]; 37 | } 38 | 39 | - (void)applicationWillTerminate:(NSNotification *)aNotification 40 | { 41 | [self destroyStreamer]; 42 | } 43 | 44 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender 45 | { 46 | return YES; 47 | } 48 | 49 | - (void)getmusiclist { 50 | [self destroyStreamer]; 51 | 52 | if ([musics count] == 0) 53 | { 54 | NSString* musiclisturl = [[NSString alloc] initWithFormat:@"http://sostart.cdn.duapp.com/api.php?_%d", (int)random()]; 55 | NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:musiclisturl]]; 56 | 57 | [request setValue:@"http://www.sostart.com/" forHTTPHeaderField:@"Referer"]; 58 | [request setValue:@"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" forHTTPHeaderField:@"User-Agent"]; 59 | [request setCachePolicy:NSURLRequestReloadIgnoringCacheData]; 60 | [request setTimeoutInterval: 60]; 61 | [request setHTTPShouldHandleCookies:FALSE]; 62 | [request setHTTPMethod:@"GET"]; 63 | 64 | aSynConnection = [aSynConnection initWithRequest:request delegate:self]; 65 | buf = [buf initWithLength:0]; 66 | } else { 67 | [musics removeObjectAtIndex:0]; 68 | 69 | if ([musics count] == 0) 70 | { 71 | [self getmusiclist]; 72 | } else { 73 | [self playmusic]; 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | - (void)playmusic { 81 | NSDictionary* music = (NSDictionary *)[musics objectAtIndex:0]; 82 | NSURL* coverurl =[[NSURL alloc] initWithString:[music objectForKey:@"cover"]]; 83 | NSImage* coverimg = [[NSImage alloc] initWithContentsOfURL:coverurl]; 84 | [self.cover setImage:coverimg]; 85 | [self.title setStringValue:[music objectForKey:@"title"]]; 86 | [self.artist setStringValue:[music objectForKey:@"artist"]]; 87 | [self createStreamer:[music objectForKey:@"source"]]; 88 | [streamer start]; 89 | } 90 | 91 | - (IBAction)play:(NSButton *)sender { 92 | if ([streamer isPlaying]) 93 | { 94 | [streamer pause]; 95 | } else { 96 | [streamer start]; 97 | } 98 | } 99 | 100 | - (IBAction)next:(NSButton *)sender { 101 | [self getmusiclist]; 102 | } 103 | 104 | - (IBAction)help:(NSButton *)sender { 105 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.sostart.com/"]]; 106 | } 107 | 108 | #pragma mark- AudioStreamer 109 | - (void)playbackStateChanged:(NSNotification *)aNotification 110 | { 111 | if ([streamer isWaiting]) 112 | { 113 | //NSLog(@"waiting"); 114 | } 115 | else if ([streamer isPlaying]) 116 | { 117 | [self.playbtn setImage:stopimg]; 118 | [self.playbtn setAlternateImage:stophighimg]; 119 | } 120 | else if ([streamer isPaused]) 121 | { 122 | [self.playbtn setImage:playimg]; 123 | [self.playbtn setAlternateImage:playhighimg]; 124 | } 125 | else if ([streamer isIdle]) 126 | { 127 | NSLog(@"stoping"); 128 | [self.playbtn setImage:playimg]; 129 | [self.playbtn setAlternateImage:playhighimg]; 130 | 131 | [self.progressbar setDoubleValue:0.0]; 132 | [self.title setStringValue:@""]; 133 | [self.artist setStringValue:@""]; 134 | [self.playtime setStringValue:@"00:00"]; 135 | [self getmusiclist]; 136 | } 137 | } 138 | 139 | - (void)updateProgress:(NSTimer *)updatedTimer 140 | { 141 | if (streamer.bitRate != 0.0) 142 | { 143 | double progress = streamer.progress; 144 | double duration = streamer.duration; 145 | 146 | if (duration > 0) 147 | { 148 | NSString* playtime = [[NSString alloc] initWithFormat:@"%02d:%02d", (int)progress / 60, (int)progress % 60]; 149 | [self.playtime setStringValue:playtime]; 150 | 151 | double st = progress / duration * 100; 152 | [self.progressbar setDoubleValue:st]; 153 | } 154 | else 155 | { 156 | } 157 | } 158 | else 159 | { 160 | } 161 | } 162 | 163 | - (void)createStreamer:(NSString *) musicpath 164 | { 165 | if (streamer) 166 | { 167 | return; 168 | } 169 | 170 | [self destroyStreamer]; 171 | 172 | NSURL *url = [NSURL URLWithString:musicpath]; 173 | streamer = [[AudioStreamer alloc] initWithURL:url]; 174 | 175 | progressUpdateTimer = 176 | [NSTimer 177 | scheduledTimerWithTimeInterval:0.1 178 | target:self 179 | selector:@selector(updateProgress:) 180 | userInfo:nil 181 | repeats:YES]; 182 | 183 | [[NSNotificationCenter defaultCenter] 184 | addObserver:self 185 | selector:@selector(playbackStateChanged:) 186 | name:ASStatusChangedNotification object:streamer]; 187 | } 188 | 189 | - (void)destroyStreamer 190 | { 191 | if (streamer) 192 | { 193 | [[NSNotificationCenter defaultCenter] 194 | removeObserver:self 195 | name:ASStatusChangedNotification 196 | object:streamer]; 197 | 198 | [streamer stop]; 199 | streamer = nil; 200 | } 201 | } 202 | 203 | #pragma mark- NSURLConnectionDelegate 204 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)aResponse { 205 | NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)aResponse; 206 | if ([httpResponse statusCode] != 200) { 207 | isok = FALSE; 208 | } else { 209 | isok = TRUE; 210 | } 211 | } 212 | 213 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 214 | if (isok) { 215 | [buf appendData:data]; 216 | } 217 | 218 | } 219 | 220 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 221 | NSLog(@"请求失败!"); 222 | isok = FALSE; 223 | } 224 | 225 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 226 | NSLog(@"获取成功!"); 227 | if(isok && [connection isEqual: aSynConnection]) 228 | { 229 | NSError* e = nil; 230 | musics = [(NSArray *)[NSJSONSerialization JSONObjectWithData:buf options:NSJSONReadingMutableContainers error:&e] mutableCopy]; 231 | [self playmusic]; 232 | 233 | isok = FALSE; 234 | } 235 | } 236 | 237 | @end 238 | -------------------------------------------------------------------------------- /sostart/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /sostart/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /sostart/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/icon-128.png -------------------------------------------------------------------------------- /sostart/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/icon-16.png -------------------------------------------------------------------------------- /sostart/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/icon-256.png -------------------------------------------------------------------------------- /sostart/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/icon-32.png -------------------------------------------------------------------------------- /sostart/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // sostart 4 | // 5 | // Created by myoula on 13-12-18. 6 | // Copyright (c) 2013年 myoula. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) 12 | { 13 | return NSApplicationMain(argc, argv); 14 | } 15 | -------------------------------------------------------------------------------- /sostart/next-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/next-high.png -------------------------------------------------------------------------------- /sostart/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/next.png -------------------------------------------------------------------------------- /sostart/play-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/play-high.png -------------------------------------------------------------------------------- /sostart/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/play.png -------------------------------------------------------------------------------- /sostart/sostart-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | icon-16.png 11 | CFBundleIdentifier 12 | com.ujiecao.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.music 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2013年 myoula. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /sostart/sostart-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #endif 10 | -------------------------------------------------------------------------------- /sostart/stop-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/stop-high.png -------------------------------------------------------------------------------- /sostart/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myoula/sostart/5c47317702c2fbac9bfc967b22f9c72f20278328/sostart/stop.png -------------------------------------------------------------------------------- /sostartTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /sostartTests/sostartTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.ujiecao.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /sostartTests/sostartTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // sostartTests.m 3 | // sostartTests 4 | // 5 | // Created by myoula on 13-12-18. 6 | // Copyright (c) 2013年 myoula. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface sostartTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation sostartTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | --------------------------------------------------------------------------------