Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I audio crossfade using cocoalibspotify?

I'd like to crossfade from one track to the next in a Spotify enabled app. Both tracks are Spotify tracks, and since only one data stream at a time can come from Spotify, I suspect I need to buffer (I think I can read ahead 1.5 x playback speed) the last few seconds of the first track, start the stream for track two, fade out one and fade in two using an AudioUnit.

I've reviewed sample apps: Viva - https://github.com/iKenndac/Viva SimplePlayer with EQ - https://github.com/iKenndac/SimplePlayer-with-EQ and tried to get my mind around the SPCircularBuffer, but I still need help. Could someone point me to another example or help bullet-point a track crossfade game plan?

Update: Thanks to iKenndac, I'm about 95% there. I'll post what I have so far:

in SPPlaybackManager.m: initWithPlaybackSession:(SPSession *)aSession {

added:

self.audioController2 = [[SPCoreAudioController alloc] init];
self.audioController2.delegate = self;

and in

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

...


self.audioController.audioOutputEnabled = self.playbackSession.isPlaying;

// for crossfade, add

self.audioController2.audioOutputEnabled = self.playbackSession.isPlaying;

and added a new method based on playTrack

-(void)crossfadeTrack:(SPTrack *)aTrack callback:(SPErrorableOperationCallback)block {
// switch audiocontroller from current to other
if (self.playbackSession.audioDeliveryDelegate == self.audioController)
{
   self.playbackSession.audioDeliveryDelegate = self.audioController2;
   self.audioController2.delegate = self;
   self.audioController.delegate = nil;
}
else
{

   self.playbackSession.audioDeliveryDelegate = self.audioController;
   self.audioController.delegate = self;
   self.audioController2.delegate = nil;
}

if (aTrack.availability != SP_TRACK_AVAILABILITY_AVAILABLE) {
    if (block) block([NSError spotifyErrorWithCode:SP_ERROR_TRACK_NOT_PLAYABLE]);
    self.currentTrack = nil;
}

self.currentTrack = aTrack;
self.trackPosition = 0.0;

[self.playbackSession playTrack:self.currentTrack callback:^(NSError *error) {

    if (!error)
        self.playbackSession.playing = YES;
    else
        self.currentTrack = nil;

    if (block) {
        block(error);
    }
}];
}

this starts a timer for crossfade

crossfadeTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self selector: @selector ( crossfadeCountdown) userInfo: nil repeats: YES];

And in order to keep the first track playing after its data has loaded in SPCoreAudioController.m I changed target buffer length:

static NSTimeInterval const kTargetBufferLength = 20;

and in SPSession.m : end_of_track(sp_session *session) {

I removed

// sess.playing = NO;

I call preloadTrackForPlayback: about 15 seconds before end of track, then crossfadeTrack: at 10 seconds before.

Then set crossfadeCountdownTime = [how many seconds you want the crossfade]*2;

I fade volume over the crosssfade with:

  - (void) crossfadeCountdown
  {

      [UIAppDelegate.playbackSPManager setVolume:(1- (((float)crossfadeCountdownTime/     (thisCrossfadeSeconds*2.0)) *0.2) )];
    crossfadeCountdownTime -= 0.5;


   if (crossfadeCountdownTime == 1.0)
   {
    NSLog(@"Crossfade countdown done");
    crossfadeCountdownTime = 0;
    [crossfadeTimer invalidate];
    crossfadeTimer = nil;
    [UIAppDelegate.playbackSPManager setVolume:1.0];

   }
 }

I'll keep working on it, and update if I can make it better. Thanks again to iKenndac for his always spot-on help!

like image 729
Dave Norfleet Avatar asked Apr 06 '13 23:04

Dave Norfleet


1 Answers

There isn't a pre-written crossfade example that I'm aware of that uses CocoaLibSpotify. However, a (perhaps not ideal) game plan would be:

  • Make two separate audio queues. SPCoreAudioController is an encapsulation of an audio queue, so you should just be able to instantiate two of them.

  • Play music as normal to one queue. When you're approaching the end of the track, call SPSession's preloadTrackForPlayback:callback: method with the next track to get it ready.

  • When all audio data for the playing track has been delivered, SPSession will fire the audio delegate method sessionDidEndPlayback:. This means that all audio data has been delivered. However, since CocoaLibSpotify buffers the audio from libspotify, there's still some time before audio stops.

  • At this point, start playing the new track but divert the audio data to the second audio queue. Start ramping down the volume of the first queue while ramping up the volume of the next one. This should give a pleasing crossfade.

A few pointers:

  • In SPCoreAudioController.m, you'll find the following line, which defines how much audio CocoaLibSpotify buffers, in seconds. If you want a bigger crossfade, you'll need to increase it.

    static NSTimeInterval const kTargetBufferLength = 0.5;
    
  • Since you get audio data at a maximum of 1.5x actual playback speed, be careful not to do, for example, a 5 second crossfade when the user has just skipped near to the end of the track. You might not have enough audio data available to pull it off.

  • Take a good look at SPPlaybackManager.m. This class is the interface between CocoaLibSpotify and Core Audio. It's not too complicated, and understanding it will get you a long way. SPCoreAudioController and SPCircularBuffer are pretty much implementation details of getting the audio into Core Audio, and you shouldn't need to understand their implementations to achieve what you want.

  • Also, make sure you understand the various delegates SPSession has. The audio delivery delegate only has one job - to receive audio data. The playback delegate gets all other playback events - when audio has finished being delivered to the audio delivery delegate, etc. There's nothing stopping one class being both, but in the current implementation, SPPlaybackManager is the playback delegate, which creates an instance of SPCoreAudioController to be the audio delivery delegate. If you modify SPPlaybackManager to have two Core Audio controllers and alternate which one is the audio delivery delegate, you should be golden.

like image 169
iKenndac Avatar answered Sep 21 '22 12:09

iKenndac