/
UKSound.m
1192 lines (951 loc) · 40.8 KB
/
UKSound.m
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
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
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
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// UKSound.m
// PlayBufferedSoundFile
//
/*
Copyright (c) 2002-2003, Kurt Revis. All rights reserved.
Modified by M. Uli Kusterer, (c) 2004.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Snoize nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//
// Acknowledgements:
// Thanks to the following people who have sent improvements and suggestions.
//
// Ben Haller -- threading bug fixes
// Steven Frank -- bug fixes
// Frank Vernon -- changes for QuickTime 4 AAC decoding (VBR)
//
/*
Renamed to fit into UliKit and added transitionVolumeTo:duration: for fades.
Report bugs to this modified version to me, so Kurt doesn't get annoyed by
being asked to fix my mistakes!
*/
#import "UKSound.h"
#import <Cocoa/Cocoa.h>
#import <AudioToolbox/DefaultAudioOutput.h>
#import <CoreAudio/CoreAudio.h>
#import <unistd.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <mach/mach_time.h>
#import "UKVirtualRingBuffer.h"
//
// Theory of operation:
//
// This class uses QuickTime to read the audio file -- getting the format as well as the actual samples.
// We then use the QuickTime SoundConverter to convert the samples to an audio format which is
// suitable for output. This may do format conversion, decompression, and sample rate conversion.
// Then, to actually play the sound, we give the converted data to the CoreAudio default audio output unit.
//
// This can be somewhat confusing. Note that there are three threads involved:
//
// 1. The calling thread (which calls -play, -stop, etc.) does some setup work, spawns off work into the other
// threads, and returns immediately. This should probably be the main thread of your application. Even if it
// isn't the main thread, you should only call these methods from one thread.
//
// 2. The feeder thread is periodically woken up. It asks the SoundConverter for a new buffer of converted data.
// The SoundConverter calls our fillSoundConverterBuffer() function when it needs more source samples. We
// provide this data by calling QuickTime (again) to read samples from the file.
//
// 3. The CoreAudio playback thread calls our renderCallback() function periodically when it needs more data
// to play.
//
// Thread 2 notifies thread 1 that it has finished playing via the NSPort signalFinishPort.
// Threads 2 and 3 communicate via the UKVirtualRingBuffer 'ringBuffer' and the int 'playbackStatus'.
// Thread 3 wakes up thread 2 using the Mach semaphore 'semaphore'.
//
// Why do we bother with the ring buffer? Because reading from the file may block, and it is
// strongly recommended not to do so while in the CoreAudio thread (which is a high-priority,
// time-constraint thread).
// Why do we bother using a separate feeder thread? Because in an application, you want the main thread
// to be free to run the UI. Also, the feeder thread needs to have a higher priority than most other threads
// in the system, to ensure that it gets time on a regular basis; it also needs to have be scheduled
// "non-timeshare" so its priority does not change dynamically.
// With this increase in priority comes more responsibility: we must not hog the CPU, or the
// performance and responsiveness of the whole system may suffer. In this case the feeder thread
// is largely IO-bound (except for any decompression that the SoundConverter does) so we're pretty safe.
//
// For more details on thread scheduling parameters, see the archives of the CoreAudio-API list
// at lists.apple.com around the date of 5 May 2002.
//
//
// Parameters to play with:
//
#define RING_BUFFER_SIZE (128 * 1024)
// Size (in bytes) of ring buffer. Should be a multiple of the page size (currently 4 KB), but it will be rounded up if necessary.
// Too big and we waste memory; too small and we run the risk of the buffer running dry (causing dropouts).
// Instead of hard-coded, this really should be computed, based on two factors: the worst-case time we expect our feeder thread to block, and the data rate of the sound file being played.
// (128 KB is about 371 milliseconds of 44.1k, 32-bit float, stereo sound.)
#define RING_BUFFER_WRITE_CHUNK_SIZE (16 * 1024)
// Size (in bytes) of chunks to write into the ring buffer. Should probably be at least a page.
// Too big and we cause writes to take too long (and the buffer may run dry while we are writing).
// Too small and we waste CPU by waking up and writing too often.
#define FILE_SAMPLE_BUFFER_SIZE (64 * 1024)
// Size (in bytes) of chunks of sample data to read from the file at one time. This is uncompressed, unconverted data.
// This is the amount we pass to GetMediaSample(), but that doesn't mean that the actual filesystem reads will necessarily be this size.
#define FEEDER_THREAD_IMPORTANCE 6
// Additional priority to use for the feeder thread, on top of this task's ordinary priority.
// This should be the lowest possible value that gives good results (no dropouts even when the machine is under load).
// The value here (6) is good on my machine (G4/450, 1 processor, OS X 10.2.1) but don't take my word for it;
// you may want to test and adjust for other machines.
// It looks like we would use 6 to get the equivalent of what iTunes uses.
@interface UKSound (Private)
// Reading file as a QuickTime movie
- (BOOL)openFileAsMovie;
- (BOOL)getMovieSoundFormat:(SoundComponentData *)outSoundFormat decompressionAtom:(AudioFormatAtomPtr *)outAtom isVBR: (BOOL*)outIsVBRPtr;
// Using the SoundConverter to translate from file samples to the audio output's format
- (BOOL)startSoundConverter;
- (void)stopSoundConverter;
static Boolean fillSoundConverterBuffer(SoundComponentDataPtr *data, void *refCon);
- (Boolean)fillSoundConverterBuffer:(SoundComponentDataPtr *)data;
// CoreAudio output
- (BOOL)setUpAudioOutput;
- (BOOL)getOutputSoundConverterFormat:(SoundComponentData *)outputSoundConverterFormat;
static UnsignedFixed ConvertFloat64ToUnsignedFixed(Float64 float64Value);
- (BOOL)startAudioOutput;
- (void)stopAudioOutput;
- (void)tearDownAudioOutput;
// File reading thread
- (void)setThreadPolicy;
- (void)fillRingBufferInThread:(id)unused;
- (void)convertIntoRingBuffer;
// Audio playback thread
static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags inActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, AudioBuffer *ioData);
- (void)renderWithFlags:(AudioUnitRenderActionFlags)flags timeStamp:(const AudioTimeStamp *)timeStamp bus:(UInt32)busNumber buffer:(AudioBuffer *)ioData;
// Control thread (finishing)
- (BOOL)checkIfInControllingThreadWithSelector:(SEL)selector;
- (void)addSignalFinishPortToControllingRunLoop;
- (void)removeSignalFinishPortFromControllingRunLoop;
- (void)handlePortMessage:(NSPortMessage *)message;
- (void)finishPlaying;
// Fade timer:
-(void) soundFadeTimerAction: (NSTimer*)timer;
@end
@implementation UKSound
enum {
statusStopped = 0,
statusReadingFromFile,
statusPaused,
statusDoneReadingFromFile,
statusDonePlaying
};
static NSString *UKSoundStoppingRunLoopMode = @"UKSoundStoppingRunLoopMode";
+ (void)initialize
{
static BOOL initialized = NO;
[super initialize];
if (!initialized) {
OSErr err;
initialized = YES;
err = EnterMovies();
if (err != noErr)
NSLog(@"+[UKSound initialize]: EnterMovies() failed with error code %hd", err);
}
}
- (id)initWithContentsOfURL:(NSURL *)url;
{
kern_return_t err;
if (![super init])
return nil;
flags.shouldLoop = NO;
flags.isStopping = NO;
url = [url standardizedURL];
// Only file URLs are handled currently
if (![url isFileURL])
goto errorReturn;
fileName = [[url path] copy];
mediaTimeLock = [[NSLock alloc] init];
soundConverterBufferFillerUPP = NewSoundConverterFillBufferDataUPP(fillSoundConverterBuffer);
converterSourceSampleDataHandle = NewHandle(FILE_SAMPLE_BUFFER_SIZE);
if (![self openFileAsMovie])
goto errorReturn;
ringBuffer = [(UKVirtualRingBuffer *)[UKVirtualRingBuffer alloc] initWithLength:RING_BUFFER_SIZE];
if (!ringBuffer)
goto errorReturn;
err = semaphore_create(mach_task_self(), &semaphore, SYNC_POLICY_FIFO, 0);
if (err) {
#if DEBUG
mach_error("semaphore_create", err);
#endif
goto errorReturn;
}
signalFinishPort = [[NSPort alloc] init];
[signalFinishPort setDelegate:self];
signalFinishPortMessage = [[NSPortMessage alloc] initWithSendPort:signalFinishPort receivePort:nil components:nil];
signalFinishRunLoopModes = [[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, UKSoundStoppingRunLoopMode, nil];
playbackStatus = statusStopped;
if (![self setUpAudioOutput])
goto errorReturn;
return self;
errorReturn:
[self release];
return nil;
}
- (id)initWithContentsOfFile:(NSString *)path;
{
NSURL *url;
url = [NSURL fileURLWithPath:path];
if (url)
return [self initWithContentsOfURL:url];
else {
[self release];
return nil;
}
}
- (void)dealloc
{
if (outputAudioUnit)
[self tearDownAudioOutput];
[fileName release];
[ringBuffer release];
if (movie)
DisposeMovie(movie);
if (converterSourceSampleDataHandle)
DisposeHandle(converterSourceSampleDataHandle);
if (semaphore)
semaphore_destroy(mach_task_self(), semaphore);
[signalFinishPort invalidate];
[signalFinishPort release];
[signalFinishPortMessage release];
[signalFinishRunLoopModes release];
[mediaTimeLock release];
[super dealloc];
}
- (BOOL)play;
{
if ([self isPlaying]) {
#if DEBUG
NSLog(@"can't play because we're already playing");
#endif
return NO;
}
if (!fileName) {
#if DEBUG
NSLog(@"can't play because there's no file");
#endif
return NO;
}
// Remember which run loop (and thus thread) is controlling playback.
controllingRunLoop = [NSRunLoop currentRunLoop];
if (![self startSoundConverter])
return NO;
// Set the initial status...
playbackStatus = statusReadingFromFile;
[ringBuffer empty];
// Now start the audio. It doesn't matter that we do this before putting any data into the ring buffer,
// since we will play silence until any data can be read from it.
if ([self startAudioOutput]) {
// When playback finishes, we will want to run some code in this thread.
// So add our port to this thread's run loop. We will send a message to the port from
// the feeder thread, causing -handlePortMessage: to be called in this thread.
[self addSignalFinishPortToControllingRunLoop];
// Start reading the file into the ring buffer, in another thread.
[NSThread detachNewThreadSelector:@selector(fillRingBufferInThread:) toTarget:self withObject:nil];
return YES;
} else {
playbackStatus = statusStopped;
[self stopSoundConverter];
return NO;
}
}
- (BOOL)pause;
{
if (![self checkIfInControllingThreadWithSelector:_cmd])
return NO;
if (playbackStatus != statusReadingFromFile)
return NO;
playbackStatus = statusPaused;
// TODO We will play the rest of the data in the ring buffer, and then play silence.
// Depending on how large the buffer is, there could be a significant length of time before sound goes silent.
// The render callback could instead check if (playbackStatus == statusPaused), and then play silence
// without removing from the buffer. That gives us immediate pausing, but there's another problem:
// if we change the playback position while paused, and then resume, we hear the old audio from the buffer
// before we hear the audio at the new position.
// The solution is to empty the buffer when appropriate, but that will be tricky, since it is only safe
// to empty the ring buffer when we can guarantee that no one is reading or writing it.
// Setting playbackStatus is not enough to do that.
return YES;
}
- (BOOL)resume;
{
if (![self checkIfInControllingThreadWithSelector:_cmd])
return NO;
if (playbackStatus != statusPaused)
return NO;
playbackStatus = statusReadingFromFile;
return YES;
}
- (BOOL)stop;
{
if (![self checkIfInControllingThreadWithSelector:_cmd])
return NO;
if (![self isPlaying]) {
#if DEBUG
NSLog(@"can't stop because we're already stopped");
#endif
return NO;
}
// Tell the feeder and render threads to stop.
playbackStatus = statusDoneReadingFromFile;
flags.isStopping = YES;
// If we are still playing, wait until we are completely stopped.
// Run the current run loop in a private mode until the feeder thread sends the signalFinishPortMessage.
// We limit waiting to a few seconds so we don't stall forever if the other thread is stuck for some reason.
if ([self isPlaying])
[[NSRunLoop currentRunLoop] runMode:UKSoundStoppingRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
// Now the stop process is complete
flags.isStopping = NO;
return (![self isPlaying]);
}
- (BOOL)isPlaying
{
return (playbackStatus != statusStopped);
}
- (BOOL)isPaused
{
return (playbackStatus == statusPaused);
}
- (id)delegate
{
return nonretainedDelegate;
}
- (void)setDelegate:(id)aDelegate
{
nonretainedDelegate = aDelegate;
}
- (float)volume;
{
float volume = 1.0; // default
AudioUnitGetParameter(outputAudioUnit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, &volume);
return volume;
}
- (void)setVolume:(float)value;
{
AudioUnitSetParameter(outputAudioUnit, kHALOutputParam_Volume, kAudioUnitScope_Global, 0, value, 0);
}
- (BOOL)shouldLoop;
{
return flags.shouldLoop;
}
- (void)setShouldLoop:(BOOL)value;
{
flags.shouldLoop = value;
}
- (float)duration;
{
// convert mediaDuration to seconds
if (mediaTimeScale == 0)
return 0.0f;
else
return (double)mediaDuration / (double)mediaTimeScale;
}
- (float)playbackPosition;
{
// No need to lock mediaTimeLock; reading currentMediaTime is atomic
if (mediaTimeScale == 0)
return 0.0f;
else
return (double)currentMediaTime / (double)mediaTimeScale;
}
- (void)setPlaybackPosition:(float)value
{
TimeValue newTime;
[mediaTimeLock lock];
newTime = (TimeValue)floor(value * (double)mediaTimeScale);
if (newTime < 0)
newTime = 0;
else if (newTime > mediaDuration)
newTime = mediaDuration;
currentMediaTime = newTime;
flags.currentMediaTimeWasChanged = YES;
[mediaTimeLock unlock];
}
-(void) transitionVolumeTo: (float)value duration: (float)time stopPlaying: (BOOL)doStop
{
[self transitionVolumeTo: value duration: time stopPlaying: doStop
pausePlaying: NO];
}
-(void) transitionVolumeTo: (float)value duration: (float)time stopPlaying: (BOOL)doStop
pausePlaying: (BOOL)doPause
{
fadeDuration = time;
fadeDestVolume = value;
float volDiff = (fadeDestVolume -[self volume]);
fadeStepWidth = volDiff / (time * 10);
stopPlayingAfterFade = doStop;
pausePlayingAfterFade = doPause;
if( fadeStepWidth != 0 )
[NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(soundFadeTimerAction:) userInfo: nil repeats: YES];
else if( doStop )
[self stop];
else if( doPause )
[self pause];
}
@end
@implementation UKSound (Private)
//
// Reading file as a QuickTime movie
//
- (BOOL)openFileAsMovie;
{
NSURL *fileURL;
FSRef fsRef;
FSSpec fsSpec;
OSErr err;
short fileRefNum;
short resID = 0;
Boolean wasChanged;
Track track;
// Convert our file name to an FSSpec
fileURL = [NSURL fileURLWithPath:fileName];
if (!CFURLGetFSRef((CFURLRef)fileURL, &fsRef))
return NO;
if (FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, NULL, &fsSpec, NULL) != noErr)
return NO;
// Open the movie file
err = OpenMovieFile(&fsSpec, &fileRefNum, fsRdPerm);
if (err != noErr)
return NO;
// Instantiate the movie and close the file
err = NewMovieFromFile(&movie, fileRefNum, &resID, NULL, newMovieActive, &wasChanged);
CloseMovieFile(fileRefNum);
if (err != noErr)
return NO;
// Get the first sound track
track = GetMovieIndTrackType(movie, 1, SoundMediaType, movieTrackMediaType);
if (!track)
return NO;
// Get the sound track's media
media = GetTrackMedia(track);
if (!media)
return NO;
return YES;
}
- (BOOL)getMovieSoundFormat:(SoundComponentData *)outSoundFormat decompressionAtom:(AudioFormatAtomPtr *)outAtom isVBR: (BOOL*)outIsVBRPtr
{
OSErr err;
SoundDescriptionV1Handle sourceSoundDescription;
Handle extension;
// Get the description of the sample data
sourceSoundDescription = (SoundDescriptionV1Handle)NewHandle(0);
GetMediaSampleDescription(media, 1, (SampleDescriptionHandle)sourceSoundDescription);
err = GetMoviesError();
if (err != noErr) {
DisposeHandle((Handle)sourceSoundDescription);
return NO;
}
// Get the "magic" decompression atom.
// This extension to the SoundDescription information stores data specific to a given audio decompressor.
// Some audio decompression algorithms require a set of out-of-stream values to configure the decompressor.
extension = NewHandle(0);
err = GetSoundDescriptionExtension((SoundDescriptionHandle)sourceSoundDescription, &extension, siDecompressionParams);
if (noErr == err) {
// Copy the atom
Size size;
size = GetHandleSize(extension);
HLock(extension);
*outAtom = malloc(size);
memcpy(*outAtom, *extension, size);
HUnlock(extension);
} else {
// If it doesn't have an atom, that's OK
*outAtom = NULL;
}
// Remember the format of the audio in the movie
// (converting from a SoundDescription to a SoundComponentData)
outSoundFormat->flags = 0;
outSoundFormat->format = (*sourceSoundDescription)->desc.dataFormat;
outSoundFormat->numChannels = (*sourceSoundDescription)->desc.numChannels;
outSoundFormat->sampleSize = (*sourceSoundDescription)->desc.sampleSize;
outSoundFormat->sampleRate = (*sourceSoundDescription)->desc.sampleRate;
outSoundFormat->sampleCount = 0;
outSoundFormat->buffer = 0;
outSoundFormat->reserved = 0;
if (outIsVBRPtr)
*outIsVBRPtr = ((*sourceSoundDescription)->desc.compressionID == variableCompression);
DisposeHandle(extension);
DisposeHandle((Handle)sourceSoundDescription);
return YES;
}
//
// Using the SoundConverter to translate from file samples to the audio output's format
//
- (BOOL)startSoundConverter;
{
OSErr err;
SoundComponentData outputSoundConverterFormat;
SoundComponentData sourceSoundConverterFormat;
AudioFormatAtomPtr audioFormatAtom = NULL;
BOOL isVBR;
// Get the audio output's format, in a structure that the SoundConverter understands
if (![self getOutputSoundConverterFormat:&outputSoundConverterFormat])
goto errorReturn;
// Get the format of the input file, along with any additional decompression parameters
if (![self getMovieSoundFormat:&sourceSoundConverterFormat decompressionAtom:&audioFormatAtom isVBR: &isVBR])
goto errorReturn;
// Remember if this source is VBR or not
isSourceVBR = isVBR;
// Create the sound converter
err = SoundConverterOpen(&sourceSoundConverterFormat, &outputSoundConverterFormat, &soundConverter);
if (err != noErr || soundConverter == NULL)
goto errorReturn;
// Let the sound converter know that we can handle VBR formats.
// (This doesn't seem to be strictly necessary but the QT 6 docs say to do it.)
SoundConverterSetInfo(soundConverter, siClientAcceptsVBR, (void*)true);
// If we have a decompression atom, give it to the SoundConverter
if (audioFormatAtom) {
err = SoundConverterSetInfo(soundConverter, siDecompressionParams, audioFormatAtom);
// and get rid of it
free(audioFormatAtom);
audioFormatAtom = NULL;
// Sometimes we get the error siUnknownInfoType. (I've seen this in AIFF files which contain a chunk of type 'wave'.)
// However, we can still continue on and decode successfully.
if (err != noErr && err != siUnknownInfoType)
goto errorReturn;
}
// Start reading the movie from the beginning, and remember how long it is
mediaDuration = GetMediaDuration(media);
mediaTimeScale = GetMediaTimeScale(media);
currentMediaTime = 0;
flags.currentMediaTimeWasChanged = NO;
// Fill in converterSourceSoundComponentData so as little of it needs to change as possible later on
converterSourceSoundComponentData.desc.flags = kExtendedSoundData;
converterSourceSoundComponentData.desc.format = sourceSoundConverterFormat.format;
converterSourceSoundComponentData.desc.numChannels = sourceSoundConverterFormat.numChannels;
converterSourceSoundComponentData.desc.sampleSize = sourceSoundConverterFormat.sampleSize;
converterSourceSoundComponentData.desc.sampleRate = sourceSoundConverterFormat.sampleRate;
converterSourceSoundComponentData.desc.flags = kExtendedSoundData;
converterSourceSoundComponentData.recordSize = sizeof(ExtendedSoundComponentData);
converterSourceSoundComponentData.extendedFlags = kExtendedSoundSampleCountNotValid | kExtendedSoundBufferSizeValid;
if (isSourceVBR)
converterSourceSoundComponentData.extendedFlags |= kExtendedSoundCommonFrameSizeValid;
// Finally, begin the conversion
err = SoundConverterBeginConversion(soundConverter);
if (err != noErr)
goto errorReturn;
return YES;
errorReturn:
if (soundConverter) {
SoundConverterClose(soundConverter);
soundConverter = NULL;
}
if (audioFormatAtom)
free(audioFormatAtom);
return NO;
}
- (void)stopSoundConverter;
{
if (soundConverter)
SoundConverterClose(soundConverter);
soundConverter = NULL;
}
Boolean fillSoundConverterBuffer(SoundComponentDataPtr *data, void *refCon)
{
return [(UKSound *)refCon fillSoundConverterBuffer:data];
}
- (Boolean)fillSoundConverterBuffer:(SoundComponentDataPtr *)data;
{
long sourceBytesReturned;
long numberOfSamples;
TimeValue sourceReturnedTime, durationPerSample;
OSErr err;
Boolean success;
if (currentMediaTime >= mediaDuration) {
if (flags.shouldLoop)
currentMediaTime = 0;
else
return false;
}
[mediaTimeLock lock];
flags.currentMediaTimeWasChanged = NO;
HUnlock(converterSourceSampleDataHandle);
err = GetMediaSample(
media,
converterSourceSampleDataHandle, // sample data is returned into this handle
FILE_SAMPLE_BUFFER_SIZE, // maximum number of bytes of sample data to be returned
&sourceBytesReturned, // the number of bytes of sample data returned
currentMediaTime, // starting time of the sample to be retrieved
&sourceReturnedTime, // indicates the actual time of the returned sample data
&durationPerSample, // duration of each sample in the media
NULL, // sample description corresponding to the returned sample data (NULL to ignore)
NULL, // index value to the sample description that corresponds to the returned sample data (NULL to ignore)
0, // maximum number of samples to be returned (0 to use a value that is appropriate for the media)
&numberOfSamples, // number of samples it actually returned
NULL); // flags that describe the sample (NULL to ignore)
HLock(converterSourceSampleDataHandle);
if (noErr == err && sourceBytesReturned > 0) {
currentMediaTime = sourceReturnedTime + (durationPerSample * numberOfSamples);
converterSourceSoundComponentData.bufferSize = sourceBytesReturned;
converterSourceSoundComponentData.desc.buffer = (Byte *)*converterSourceSampleDataHandle;
// For VBR audio we specified the kExtendedSoundCommonFrameSizeValid flag,
// so we must fill in the commonFrameSize field appropriately.
// GetMediaSample always returns all frames with the same size.
if (isSourceVBR)
converterSourceSoundComponentData.commonFrameSize = sourceBytesReturned / numberOfSamples;
*data = (SoundComponentDataPtr)&converterSourceSoundComponentData;
success = true;
} else {
success = false;
}
[mediaTimeLock unlock];
return success;
}
//
// CoreAudio output
//
- (BOOL)setUpAudioOutput;
{
OSStatus err;
struct AudioUnitInputCallback inputCallbackStruct;
// Note: This is what we call "sophisticated error handling".
err = OpenDefaultAudioOutput(&outputAudioUnit);
if (err)
return NO;
err = AudioUnitInitialize(outputAudioUnit);
if (err)
return NO;
// Set up our callback to feed data to the AU.
inputCallbackStruct.inputProc = renderCallback;
inputCallbackStruct.inputProcRefCon = self;
err = AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_SetInputCallback, kAudioUnitScope_Input, 0, &inputCallbackStruct, sizeof(inputCallbackStruct));
if (err)
return NO;
return YES;
}
- (BOOL)getOutputSoundConverterFormat:(SoundComponentData *)outputSoundConverterFormat
{
OSErr err;
AudioStreamBasicDescription outputCoreAudioFormat;
UInt32 size;
// Get the format that the AudioUnit expects us to give it
size = sizeof(AudioStreamBasicDescription);
err = AudioUnitGetProperty(outputAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputCoreAudioFormat, &size);
if (err)
return NO;
// Translate the format to a SoundComponentData for the sound converter. Yuck.
outputSoundConverterFormat->flags = 0;
if (outputCoreAudioFormat.mFormatID != kAudioFormatLinearPCM)
return NO;
if (outputCoreAudioFormat.mFormatFlags & kLinearPCMFormatFlagIsFloat) {
if (outputCoreAudioFormat.mBitsPerChannel == 32)
outputSoundConverterFormat->format = kFloat32Format;
else if (outputCoreAudioFormat.mBitsPerChannel == 64)
outputSoundConverterFormat->format = kFloat64Format;
else
return NO;
} else {
BOOL isBigEndian = (outputCoreAudioFormat.mFormatFlags & kLinearPCMFormatFlagIsBigEndian);
if (outputCoreAudioFormat.mBitsPerChannel == 16)
outputSoundConverterFormat->format = (isBigEndian ? k16BitBigEndianFormat : k16BitLittleEndianFormat);
else if (outputCoreAudioFormat.mBitsPerChannel == 32)
outputSoundConverterFormat->format = (isBigEndian ? k32BitFormat : k32BitLittleEndianFormat);
else
return NO;
}
outputSoundConverterFormat->numChannels = outputCoreAudioFormat.mChannelsPerFrame;
outputSoundConverterFormat->sampleSize = outputCoreAudioFormat.mBitsPerChannel;
outputSoundConverterFormat->sampleRate = ConvertFloat64ToUnsignedFixed(outputCoreAudioFormat.mSampleRate);
outputSoundConverterFormat->sampleCount = 0;
outputSoundConverterFormat->buffer = NULL;
outputSoundConverterFormat->reserved = 0;
return YES;
}
UnsignedFixed ConvertFloat64ToUnsignedFixed(Float64 float64Value)
{
UnsignedFixed fixedValue;
// High 2 bytes is the integer part of the value
// Low 2 bytes is the floating point part of the value
fixedValue = ((UInt32)float64Value << 16) + ((UInt16)((float64Value - floor(float64Value)) * 65536.0));
return fixedValue;
}
- (BOOL)startAudioOutput;
{
return (noErr == AudioOutputUnitStart(outputAudioUnit));
}
- (void)stopAudioOutput;
{
// Don't bother checking for errors -- we can't do anything about them anyway.
if (outputAudioUnit)
AudioOutputUnitStop(outputAudioUnit);
}
- (void)tearDownAudioOutput;
{
// Don't bother checking for errors -- we can't do anything about them anyway.
if (outputAudioUnit)
CloseComponent(outputAudioUnit);
outputAudioUnit = NULL;
}
//
// File reading thread
// (an ordinary thread, managed by this app)
//
- (void)setThreadPolicy;
{
// Increase this thread's priority, and turn off timesharing. See the notes at the top of this file.
kern_return_t error;
thread_extended_policy_data_t extendedPolicy;
thread_precedence_policy_data_t precedencePolicy;
extendedPolicy.timeshare = 0;
error = thread_policy_set(mach_thread_self(), THREAD_EXTENDED_POLICY, (thread_policy_t)&extendedPolicy, THREAD_EXTENDED_POLICY_COUNT);
if (error != KERN_SUCCESS) {
#if DEBUG
mach_error("Couldn't set feeder thread's extended policy", error);
#endif
}
precedencePolicy.importance = FEEDER_THREAD_IMPORTANCE;
error = thread_policy_set(mach_thread_self(), THREAD_PRECEDENCE_POLICY, (thread_policy_t)&precedencePolicy, THREAD_PRECEDENCE_POLICY_COUNT);
if (error != KERN_SUCCESS) {
#if DEBUG
mach_error("Couldn't set feeder thread's precedence policy", error);
#endif
}
}
- (void)fillRingBufferInThread:(id)unused
{
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];
[self setThreadPolicy];
NS_DURING {
mach_timespec_t timeout = { 2, 0 }; // 2 seconds, 0 nanoseconds
// While there is still data to be read from the file, fill as much of the ring buffer as is practical.
// Then sleep until the playback thread wakes us up; at that time, there will be space to write into the buffer again,
// or playbackStatus will be set to statusDonePlaying.
while (playbackStatus != statusDonePlaying) {
if (playbackStatus == statusReadingFromFile)
[self convertIntoRingBuffer];
// Wait for the audio thread to signal us that it could use more data, or for the timeout to happen
semaphore_timedwait(semaphore, timeout);
}
// Now we are done playing sound, but we still need to clean things up in the control thread.
#if 0
// This should work but it doesn't. If it did, we could avoid all the NSPort stuff...
// Bugs have been filed with Apple: 3157666 and 3157696.
[controllingRunLoop performSelector:@selector(finishPlaying) target:self argument:nil order:0 modes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, UKSoundStoppingRunLoopMode, nil]];
#else
[signalFinishPortMessage sendBeforeDate:[NSDate distantFuture]];
#endif
} NS_HANDLER {
NSLog(@"UKSound: exception raised in fillRingBufferInThread: %@", localException);
} NS_ENDHANDLER;
[pool release];
}
- (void)convertIntoRingBuffer
{
// Check if there is a reasonable amount of space available to write to the ring buffer.
// If there is, ask the SoundConverter to put a chunk of converted data into the ring buffer.
// Repeat this until the available space to write is too small, or we run out of data to convert.
BOOL continueReading;
do {
UInt32 bytesToWrite, bytesAvailableToWrite;
void *writePointer;
continueReading = NO;
bytesToWrite = RING_BUFFER_WRITE_CHUNK_SIZE;
bytesAvailableToWrite = [ringBuffer lengthAvailableToWriteReturningPointer:&writePointer];
if (bytesAvailableToWrite >= bytesToWrite) {
OSErr err = noErr;
UInt32 bytesWritten;
UInt32 framesWritten;
UInt32 outputFlags;
if (flags.currentMediaTimeWasChanged) {
// Throw away any data that may have been buffered inside the SoundConverter,
// and restart the conversion at the new time.
UInt32 discardBytesWritten, discardFramesWritten;
err = SoundConverterEndConversion(soundConverter, NULL, &discardFramesWritten, &discardBytesWritten);
if (err == noErr)
err = SoundConverterBeginConversion(soundConverter);
}
if (err == noErr) {
// Request the sound converter to convert one buffer of data.
err = SoundConverterFillBuffer(soundConverter,
soundConverterBufferFillerUPP,
self,
writePointer,
bytesToWrite,
&bytesWritten,
&framesWritten,
&outputFlags);
}
if (err != noErr) {
// Act like there's no more data
bytesWritten = 0;
outputFlags = kSoundConverterDidntFillBuffer;
#if DEBUG
NSLog(@"SoundConverterFillBuffer returned error %d, terminating playback", err);
#endif
}
if (bytesWritten > 0)
[ringBuffer didWriteLength:bytesWritten];
if (outputFlags & kSoundConverterDidntFillBuffer) {
// We have read the last of the file (or hit an error in reading it, or EOF). So now we're done.
playbackStatus = statusDoneReadingFromFile;
} else {
// Immediately go back around for another chunk.
continueReading = YES;
}
}
} while (continueReading);
}
//