10

I just want to get a list of the markers in an audio file. I thought this would be an easy common task that wouldn't be too difficult. However, I can barely find any example code or documentation, so I ended up with this:

private func getMarkers(_ url: CFURL) -> AudioFileMarkerList {

  var file: AudioFileID?
  var size: UInt32 = 0
  var markers = AudioFileMarkerList()

  AudioFileOpenURL(url, .readPermission, kAudioFileWAVEType, &file)
  AudioFileGetPropertyInfo(file!, kAudioFilePropertyMarkerList, &size, nil)
  AudioFileGetProperty(file!, kAudioFilePropertyMarkerList, &size, &markers)

  return markers
}

Sadly, this doesn't work: error: memory read failed for 0x0.

I just can't figure out the problem. I checked the url and the size (which are both valid), but it always fails to retrieve the markers. Any help with this would be fantastic!

EDIT: This sort of works, but all the data is completely wrong, and I can't understand how a single audio file can have multiple AudioFileMarkerLists of markers:

private func getMarkers(_ url: CFURL) -> [AudioFileMarkerList] {

  var file: AudioFileID?
  var size: UInt32 = 0

  AudioFileOpenURL(url, .readPermission, kAudioFileWAVEType, &file)
  AudioFileGetPropertyInfo(file!, kAudioFilePropertyMarkerList, &size, nil)

  let length = NumBytesToNumAudioFileMarkers(Int(size))
  var markers = [AudioFileMarkerList](repeating: AudioFileMarkerList(), count: length)
  AudioFileGetProperty(file!, kAudioFilePropertyMarkerList, &size, &markers)
  return markers
}

EDIT 2: According to most answers I've seen so far, this should work, but it returns an empty array:

private func getMarkers(_ url: CFURL) -> [AudioFileMarkerList] {

  var file: AudioFileID?
  var size: UInt32 = 0

  AudioFileOpenURL(url, .readPermission, kAudioFileWAVEType, &file)
  AudioFileGetPropertyInfo(file!, kAudioFilePropertyMarkerList, &size, nil)
  let length = NumBytesToNumAudioFileMarkers(Int(size))

  var markers = [AudioFileMarkerList]()
  markers.reserveCapacity(length)
  AudioFileGetProperty(file!, kAudioFilePropertyMarkerList, &size, &markers)

  return markers

}

EDIT 3: I got rid of a bunch of error checking and useful stuff from Ryan's code for anyone wanting to quickly try and find the problem:

private func getMarkers(_ url: CFURL) -> [AudioFileMarker]? {

    var file: AudioFileID?
    var size: UInt32 = 0
    var markers: [AudioFileMarker] = []

    AudioFileOpenURL(url, .readPermission, kAudioFileWAVEType, &file)

    AudioFileGetPropertyInfo(file!, kAudioFilePropertyMarkerList, &size, nil)

    let length = NumBytesToNumAudioFileMarkers(Int(size))

    let data = UnsafeMutablePointer<AudioFileMarkerList>.allocate(capacity: length)

    AudioFileGetProperty(file!, kAudioFilePropertyMarkerList, &size, data)

    markers.append(data.pointee.mMarkers)

    data.deallocate(capacity: length)

    return markers
}

I just hope Apple actually tested AudioFileMarkerList in the first place.

EDIT 4: SOLVED thanks to Rhythmic Fistman and Ryan Francesconi! Final result:

private func getMarkers(_ url: CFURL) -> [AudioFileMarker]? {

  var file: AudioFileID?
  var size: UInt32 = 0
  var markerList: [AudioFileMarker] = []

  AudioFileOpenURL(url, .readPermission, kAudioFileWAVEType, &file)

  AudioFileGetPropertyInfo(file!, kAudioFilePropertyMarkerList, &size, nil)

  let length = NumBytesToNumAudioFileMarkers(Int(size))

  let data = UnsafeMutablePointer<AudioFileMarkerList>.allocate(capacity: length)

  AudioFileGetProperty(file!, kAudioFilePropertyMarkerList, &size, data)

  let markers = UnsafeBufferPointer<AudioFileMarker>(start: &data.pointee.mMarkers, count: length)
  for marker in markers {
    markerList.append(marker)
  }

  data.deallocate(capacity: length)

  return markerList
}
MysteryPancake
  • 1,365
  • 1
  • 18
  • 47
  • 1
    Why that 'repeating'? –  Jun 25 '17 at 07:20
  • @3000 All the other answers I find use Objective-C, which lets you allocate memory to an array. Swift does not. This was just my attempt to try and do what Objective-C does. It doesn't seem to work otherwise. – MysteryPancake Jun 25 '17 at 07:23
  • 1
    https://developer.apple.com/documentation/swift/array/1538966-reservecapacity –  Jun 25 '17 at 07:26
  • @3000 Interesting... but the problem remains - if I use that, the array is empty and nothing is written into it (at least it seems this way when printing the array) – MysteryPancake Jun 25 '17 at 07:32
  • 1
    As far as I understand, you fill an array with length instances of new AudioFileMarkerList instances, then you return them. Is this what you want? –  Jun 25 '17 at 07:44
  • 1
    @3000 All I really want is to get a list of all the markers in an audio file, but to me it looks like I'm creating a list of markers for each marker (is this even what AudioFileMarkerList is?). The main problem is that all the data is incorrect. I tried using this function on an audio file with markers, and most of the names are null, and all the timing is completely off. The file isn't corrupt, but there's something wrong with the way I'm using AudioFileMarkerList. I don't understand how it works at all – MysteryPancake Jun 25 '17 at 07:53
  • Bump, still can't find a solution – MysteryPancake Jun 27 '17 at 04:27
  • 1
    Take a look to the instance properties here: https://developer.apple.com/documentation/audiotoolbox/audiofilemarkerlist?language=objc –  Jun 27 '17 at 05:46
  • @3000 I've read that already, but it doesn't really help me figure out what I'm supposed to be doing. Please tell me if I missed something – MysteryPancake Jun 27 '17 at 06:28
  • 1
    I don't know because I generally don't work with audios, so I've very little knowledge of this part of the API –  Jun 27 '17 at 07:16
  • 1
    @3000 Neither have I. Thanks for responding though, I'll try to work it out – MysteryPancake Jun 27 '17 at 07:33
  • Someone please help – MysteryPancake Jul 01 '17 at 06:42
  • 1
    Try asking in the official developers Apple forum –  Jul 01 '17 at 08:07
  • 1
    @3000 I did https://forums.developer.apple.com/message/240187 – MysteryPancake Jul 01 '17 at 08:35
  • 1
    May I suggest one thing? Post a working Obj-c code and ask them how to perfectly convert it to Swift 3 –  Jul 01 '17 at 08:46
  • 1
    I've tried many pieces of code and I've tried many translation methods, but I haven't asked directly yet. Thanks for the idea and all the help so far! – MysteryPancake Jul 01 '17 at 08:47
  • 1
    Bumping with bounty – MysteryPancake Aug 10 '17 at 00:51
  • 1
    Can you put your runnable sample code up on GitHub for us to try? – Rhythmic Fistman Aug 17 '17 at 02:19
  • @RhythmicFistman Here you go https://www.dropbox.com/s/b6js3v8stlgcqyp/stackoverflow.playground.zip?raw=1 – MysteryPancake Aug 17 '17 at 23:10
  • 1
    Thanks, the problem was that `mMarkers` is a variable length array, see my answer. – Rhythmic Fistman Aug 17 '17 at 23:30

2 Answers2

3

Looks like you need to use UnsafeBufferPointer to access variable length arrays (like mMarkers). So instead of

out.append(markerList.mMarkers)

which only adds the first element, do this

let markersBuffer = UnsafeBufferPointer<AudioFileMarker>(start: &data.pointee.mMarkers,
                                               count: Int(data.pointee.mNumberMarkers))

for marker in markersBuffer {
    markers.append(marker)
}

Modeled on this answer

Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
1

EDIT: Simplest solution is to use AudioKit's version of EZAudioFile.markers. Note this is not the same as the original EZAudio framework as I had added this marker code to AudioKit's version only.

import AudioKit
...

if let file = EZAudioFile(url: url) {
    if let markers = file.markers as? [EZAudioFileMarker] {
        for m in markers {
            Swift.print("NAME: \(m.name) FRAME: \(m.framePosition)")
        }
    }
}

If you REALLY want to try in Swift, it would look something like the below. I'm not an expert in this, but as far as I can tell, there is some issue translating the AudioFileMarkerList struct to Swift. This may be solvable, but it seems to me it's best to just use Objective C to accomplish these calls. Here is the almost finished function in Swift. I recommend using AudioKit to accomplish what you need as I have added the marker code to EZAudioFile there. Check: https://github.com/AudioKit/AudioKit/blob/master/AudioKit/Common/Internals/EZAudio/EZAudioFile.m

But for the record here is the Swift code in progress! Note it's hard coded to WAVE files for the moment... Perhaps someone else can finish this?

class func getAudioFileMarkers(_ url: URL) -> [AudioFileMarker]? {
    Swift.print("getAudioFileMarkers() \(url)")

    var err: OSStatus = noErr
    var audioFileID: AudioFileID?

    err = AudioFileOpenURL(url as CFURL,
                           .readPermission,
                           kAudioFileWAVEType,
                           &audioFileID)

    if err != noErr {
        Swift.print("AudioFileOpenURL FAILED, Error: \(err)")
        return nil
    }

    guard audioFileID != nil else {
        return nil
    }

    Swift.print("audioFileID: \(audioFileID)")

    var outSize: UInt32 = 0
    var writable: UInt32 = 0

    err = AudioFileGetPropertyInfo(audioFileID!, kAudioFilePropertyMarkerList, &outSize, &writable)
    if err != noErr {
        Swift.print("AudioFileGetPropertyInfo kAudioFilePropertyMarkerList FAILED, Error: \(err)")
        return nil
    }

    Swift.print("outSize: \(outSize), writable: \(writable)")

    guard outSize != 0 else { return nil }

    let length = NumBytesToNumAudioFileMarkers( Int(outSize) )

    Swift.print("Found \(length) markers")

    let theData = UnsafeMutablePointer<AudioFileMarkerList>.allocate(capacity: length)

    if length == 0 {
        return nil
    }

    // pull marker list
    err = AudioFileGetProperty(audioFileID!, kAudioFilePropertyMarkerList, &outSize, theData)

    if err != noErr {
        Swift.print("AudioFileGetProperty kAudioFilePropertyMarkerList FAILED, Error: \(err)")
        return nil
    }

    let markerList: AudioFileMarkerList = theData.pointee

    Swift.print("markerList.mMarkers: \(markerList.mMarkers)")
    // this is only showing up as a single AudioFileMarker, not an array of them.
    // I DON'T KNOW WHY. It works in Obj-C. I'm obviously missing something, or there is a problem in translation

    var out = [AudioFileMarker]()

    let mirror = Mirror(reflecting: markerList.mMarkers)
    for m in mirror.children {
        Swift.print( "label: \(m.label) value: \(m.value)" )
    }

    // for now just append the first one.
    // :(
    out.append(markerList.mMarkers)

    // done with this now
    theData.deallocate(capacity: length)

    return out
}
Ryan Francesconi
  • 988
  • 7
  • 17
  • 1
    Do you think you could report/ask this question to Apple? It seems to be an internal bug that I can't ask about since I don't have a developer subscription – MysteryPancake Jul 29 '17 at 07:17
  • 1
    Why not just use Objective C? But yes, it'd be good to file a bug with them too. – Ryan Francesconi Jul 30 '17 at 03:30
  • 1
    I don't recommend the code I posted as it's basically just psuedo C. It's better to just use C for this directly. This is a simple thing to do from Swift, so I don't see why you're not doing it. – Ryan Francesconi Jul 30 '17 at 03:35
  • 1
    Check my edit. Use AudioKit, or if you don't want to embed the framework, you can use the EZAudioFile* classes directly from the framework source. Example project: https://u101308273.dl.dropboxusercontent.com/u/101308273/MarkerTest.zip – Ryan Francesconi Jul 30 '17 at 03:56
  • The reason I don't want to use C is because I haven't used anything except Swift so far for any part of the app (including audio-related stuff). I don't even know how C works, and if I change something it'll probably break everything. Also, if there's any bugs with the function at any point in the future, I'll have no clue what they are or how to fix them. – MysteryPancake Jul 31 '17 at 07:45
  • 1
    Honestly... you should do what I suggest. Calling this stuff in Swift is using C just with a different syntax that is more unstable if you don't understand how to manage memory. Someone could make a better swift wrapper, but at the end of the day it's going to be C that is called. Better if you do it directly yourself. If there is any doubt, use the EZAudio method I mention. It is solid. – Ryan Francesconi Aug 01 '17 at 04:43
  • 1
    One more option which I didn't mention is that if you're only working with Wave files, you can scan the LIST chunk and pull the markers that way. Dealing with markers is lower level that you want to go, but if you need this information, at some point you're going to have to get your hands dirty and learn it. For a good wave framework, use https://github.com/djehuti/WaveTools – Ryan Francesconi Aug 01 '17 at 04:46
  • I just wanted to tell you that Rhythmic Fistman found the solution, just in case you were wondering. I updated my original post. – MysteryPancake Aug 18 '17 at 06:10
  • not that you weren't an immense help as well and already solved 90% of the problem. – MysteryPancake Aug 19 '17 at 05:06