17

A lot of the constants associated with Audio Session programming are really four-character strings (Audio Session Services Reference). The same applies to the OSStatus code returned from functions like AudioSessionGetProperty.

The problem is that when I try to print these things out of the box, they look like 1919902568. I can plug that into Calculator and turn on ASCII output and it'll tell me "roch", but there must be a programmatic way to do this.

I've had limited success in one of my C functions with the following block:

char str[20];
// see if it appears to be a four-character code
*(UInt32 *) (str + 1) = CFSwapInt32HostToBig(error);
if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) {
    str[0] = str[5] = '\'';
    str[6] = '\0';
} else {
    // no, format as integer
    sprintf(str, "%d", (int)error);
}

What I want to do is to abstract this feature out of its current function, in order to use it elsewhere. I tried doing

char * fourCharCode(UInt32 code) {
    // block
}
void someOtherFunction(UInt32 foo){
    printf("%s\n",fourCharCode(foo));
}

but that gives me "à*€/3íT:ê*€/+€/", not "roch". My C fu isn't very strong, but my hunch is that the above code tries to interpret the memory address as a string. Or perhaps there's an encoding issue? Any ideas?

hippietrail
  • 15,848
  • 18
  • 99
  • 158
Spencer Williams
  • 902
  • 8
  • 26

16 Answers16

20

The type you're talking about is a FourCharCode, defined in CFBase.h. It's equivalent to an OSType. The easiest way to convert between OSType and NSString is using NSFileTypeForHFSTypeCode() and NSHFSTypeCodeFromFileType(). These functions, unfortunately, aren't available on iOS.

For iOS and Cocoa-portable code, I like Joachim Bengtsson's FourCC2Str() from his NCCommon.h (plus a little casting cleanup for easier use):

#include <TargetConditionals.h>
#if TARGET_RT_BIG_ENDIAN
#   define FourCC2Str(fourcc) (const char[]){*((char*)&fourcc), *(((char*)&fourcc)+1), *(((char*)&fourcc)+2), *(((char*)&fourcc)+3),0}
#else
#   define FourCC2Str(fourcc) (const char[]){*(((char*)&fourcc)+3), *(((char*)&fourcc)+2), *(((char*)&fourcc)+1), *(((char*)&fourcc)+0),0}
#endif

FourCharCode code = 'APPL';
NSLog(@"%s", FourCC2Str(code));
NSLog(@"%@", @(FourCC2Str(code));

You could of course throw the @() into the macro for even easier use.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Ooh, that sounds really useful. It gives me an error "Expected expression" out of the box though, so I'll have to come back to this. – Spencer Williams Jan 08 '13 at 20:05
  • Cleaned up to make it a little easier to use. It returns a char[] rather than a char*, and that seems to confuse things now. – Rob Napier Jan 08 '13 at 20:41
  • 2
    `NSFileTypeForHFSTypeCode` for `1634039412` annoyingly returns a 6-length `"\'aevt\'"` with the single quotes included :/ – pkamb Aug 20 '19 at 21:13
7

In Swift you would use this function:

func str4 (n: Int) -> String
{
    var s: String = ""
    var i: Int = n

    for var j: Int = 0; j < 4; ++j
    {
        s = String(UnicodeScalar(i & 255)) + s
        i = i / 256
    }

    return (s)
}

This func will do the same like above in a third of the time:

func str4 (n: Int) -> String
{
    var s: String = String (UnicodeScalar((n >> 24) & 255))
    s.append(UnicodeScalar((n >> 16) & 255))
    s.append(UnicodeScalar((n >> 8) & 255))
    s.append(UnicodeScalar(n & 255))
    return (s)
}

The reverse way will be:

func val4 (s: String) -> Int
{
    var n: Int = 0
    var r: String = ""
    if (countElements(s) > 4)
    {
        r = s.substringToIndex(advance(s.startIndex, 4))
    }
    else
    {
        r = s + "    "
        r = r.substringToIndex(advance(r.startIndex, 4))
    }
    for UniCodeChar in r.unicodeScalars
    {
        n = (n << 8) + (Int(UniCodeChar.value) & 255)
    }

    return (n)
}
j.s.com
  • 1,422
  • 14
  • 24
  • Just to simplify one of your functions [code]func stringValue(unicodeValue: Int) -> String { var stringValue = "" var value = unicodeValue for _ in 0..<4 { stringValue = String(UnicodeScalar(value & 255)) + stringValue value = value / 256 } return stringValue }[\code] – TheCodingArt Sep 03 '15 at 18:17
  • Another way: extension Int { var unicodeStringValue: String { get { var stringValue = "" var value = self for _ in 0..<4 { stringValue = String(UnicodeScalar(value & 255)) + stringValue value = value / 256 } return stringValue } } } – TheCodingArt Sep 03 '15 at 18:24
4
char str[5];
str[4] = '\0';
long *code = (long *)str;
*code = 1919902568;
printf("%s\n", str);
s.bandara
  • 5,636
  • 1
  • 21
  • 36
  • 1
    After figuring out a reverseStr function, this works. I'd really like to get a DRYer solution (like to wrap this in a method), but it serves for now. – Spencer Williams Jan 08 '13 at 19:43
  • `long *code = (long *)str; *code = 1919902568;` is a strict aliasing violation and invokes undefined behavior. Additionally, merely creating the pointer `long *code = (long *)str;` can also invoke undefined behavior if any platform-specific alignment requirements are not met - note that the UB is invoked immediately on pointer creation in that case and no dereference is needed to invoke UB. – Andrew Henle Aug 09 '23 at 11:13
4

Integer = 4 bytes = 4 chars. So to convert from integer to char * you can simply write:

char st[5] = {0};
st[0] = yourInt & 0xff;
st[1] = (yourInt >> 8) & 0xff;
st[2] = (yourInt >> 16) & 0xff;
st[3] = (yourInt >> 24) & 0xff;

To convert it back:

yourInt = st[0] | (st[1] << 8) | (st[2] << 16) | (st[3] << 24);
Nickolay Olshevsky
  • 13,706
  • 1
  • 34
  • 48
  • wouldn't it be good to add `st[4] = '\0';` in case someone wants to print the string? – Jan Rüegg Sep 23 '16 at 17:00
  • 1
    May seem simple but the approach is wrong. The first character is the highest byte. `#define FourCC2Str(code) (char[5]){(code >> 24) & 0xFF, (code >> 16) & 0xFF, (code >> 8) & 0xFF, code & 0xFF, 0}` – Paul B. Nov 02 '18 at 19:27
  • It is not wrong since most CPUs, including Intel ones, are little-endian nowadays. – Nickolay Olshevsky Nov 06 '18 at 10:45
  • It is unadvisable, because you do not know that st[4] is yours to change. It is OUTSIDE the memory where your FourBytesCode is stored. Remember that these are almost always simple Int32 variables, and not "memory blocks", and this byte you are zeroing could easily be part of another variable or just crash your program immediately. – Motti Shneor Dec 06 '21 at 10:53
  • @JanRüegg st[4] is already `\0` due to `= {0}` initialization. @MottiShneor why st[4] cannot be yours once it is on the stack? – Nickolay Olshevsky Dec 06 '21 at 11:06
4

I wrote this C function for my audio code ... it might be a tad naive, but it does the job for well enough for me:

NSString* fourCharNSStringForFourCharCode(FourCharCode aCode){

char fourChar[5] = {(aCode >> 24) & 0xFF, (aCode >> 16) & 0xFF, (aCode >> 8) & 0xFF, aCode & 0xFF, 0};

NSString *fourCharString = [NSString stringWithCString:fourChar encoding:NSUTF8StringEncoding];

return fourCharString; }
OverToasty
  • 749
  • 7
  • 15
3

I suggest using a function like this:

static NSString * NSStringFromCode(UInt32 code)
{
    UInt8 chars[4];
    *(UInt32 *)chars = code;
    for(UInt32 i = 0; i < 4; ++i)
    {
        if(!isprint(chars[i]))
        {
            return [NSString stringWithFormat:@"%u", code];
        }
    }
    return [NSString stringWithFormat:@"%c%c%c%c", chars[3], chars[2], chars[1], chars[0]];
}

This will ensure that you don't end up with some random results for some FourCharCodes that are actual numbers like kCMPixelFormat_32ARGB = 32.

McZonk
  • 1,390
  • 13
  • 14
3

In case you're developing for macOS rather than iOS, there's already a built-in function for this in Launch Services. To get the 4cc as NSString:

(__bridge_transfer NSString *)UTCreateStringForOSType(fourccInt)
svth
  • 1,303
  • 1
  • 14
  • 23
  • Same as existing answer by Charlesism. – matt Apr 26 '17 at 18:05
  • There is no answer by anyone with that name, and no answer mentioning UTCreateStringForOSType/() – svth Apr 27 '17 at 20:57
  • He deleted it 10 hours ago. Nevertheless, it is right for macOS, and was of great help to me in writing a macOS application. As his answer rightly said, however, the original question is about iOS, where UTCreateStringForOSType — so your answer suffers from that issue as well. – matt Apr 27 '17 at 21:06
  • This is the best answer so far. reason: it is THE INTENDED function to use, created and maintained by Apple, and so - future-changes-safe. Also, I believe it should be efficient, and its name is descriptive. Been looking for this all over the place – Motti Shneor Jul 06 '21 at 15:52
  • Sadly Apple has deprecated it in macOS 12. Sigh. – dmaclach Feb 26 '22 at 06:02
3

Here are my helper functions I use within test targets:

Swift 5:

extension FourCharCode {
    private static let bytesSize = MemoryLayout<Self>.size
    var codeString: String {
        get {
            withUnsafePointer(to: bigEndian) { pointer in
                pointer.withMemoryRebound(to: UInt8.self, capacity: Self.bytesSize) { bytes in
                    String(bytes: UnsafeBufferPointer(start: bytes,
                                                      count: Self.bytesSize),
                           encoding: .macOSRoman)!
                }
            }
        }
    }
}

extension OSStatus {
    var codeString: String {
        FourCharCode(bitPattern: self).codeString
    }
}

private func fourChars(_ string: String) -> String? {
    string.count == MemoryLayout<FourCharCode>.size ? string : nil
}
private func fourBytes(_ string: String) -> Data? {
    fourChars(string)?.data(using: .macOSRoman, allowLossyConversion: false)
}
func stringCode(_ string: String) -> FourCharCode {
    fourBytes(string)?.withUnsafeBytes { $0.load(as: FourCharCode.self).byteSwapped } ?? 0
}

It works for me both on macOS and iOS and closely matches the behaviour of built-in macOS NSFileTypeForHFSTypeCode and NSHFSTypeCodeFromFileType

A couple of gotchas:

  1. use bigEndian and byteSwapped
  2. use macOSRoman encoding
  3. handle long strings by returning 0, as NSHFSTypeCodeFromFileType

The difference of above implementation from standard library methods:

  1. NSFileTypeForHFSTypeCode adds extra single quotes around the sting: NSFileTypeForHFSTypeCode(OSType(kAudioCodecBadDataError))) == "'bada'". I've opted out to just return bada
  2. it fails to produce strings with \0 in the middle, like in NSFileTypeForHFSTypeCode(kAudioFormatMicrosoftGSM) should be ms\01 but returns 'ms
Paul Zabelin
  • 225
  • 2
  • 6
2

A more Swifty implementation, working for Swift 4:

extension String {
    init(fourCharCode: FourCharCode) {
        let n = Int(fourCharCode)
        var s: String = ""

        let unicodes = [UnicodeScalar((n >> 24) & 255), UnicodeScalar((n >> 16) & 255), UnicodeScalar((n >> 8) & 255), UnicodeScalar(n & 255)]
        unicodes.flatMap { (unicode) -> String? in
            guard let unicode = unicode else { return nil }
            return String(unicode)
        }.forEach { (unicode) in
            s.append(unicode)
        }

        self = s.trimmingCharacters(in: CharacterSet.whitespaces)
    }
}

Which can be used as followed:

String(fourCharCode: CMFormatDescriptionGetMediaSubType(charCode))
Antoine
  • 23,526
  • 11
  • 88
  • 94
2
  • Handles different endian architectures
  • iOS and MacOS(et al)
  • Easy to understand
  • Resulting string (should be) identical to OSType even with 8-bit characters
#import <Cocoa/Cocoa.h>
#import <CoreServices/CoreServices.h> // for EndianU32_NtoB
@implementation NSString (FourCC)

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    NSString * result = nil;
    cccc = EndianU32_NtoB(cccc); // convert to network byte order, if needed
    NSData * data = [NSData dataWithBytes: &cccc length: sizeof(OSType)];
    result = [[NSString alloc] initWithData: data encoding: NSWindowsCP1252StringEncoding]; // lossless 8-bit encoding, could also use NSMacOSRomanStringEncoding
    return result;
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
pawneebill
  • 171
  • 6
  • See preceding text box, and integral comments. If that does not suffice, please ask a specific question. – pawneebill Jul 09 '19 at 21:21
  • I think you could avoid creating NSData wrapper object, and build NSString directly from "bytes" using: - (nullable instancetype)initWithBytes:(const void *)bytes length:(NSUInteger)len encoding:(NSStringEncoding)encoding; – Motti Shneor Dec 06 '21 at 09:33
1
public extension UInt32 {
    var string: String {
        String([
            Character(Unicode.Scalar(self >> 24 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self >> 16 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self >> 8 & 0xFF) ?? "?"),
            Character(Unicode.Scalar(self & 0xFF) ?? "?")
        ])
    }
}
Moose
  • 2,607
  • 24
  • 23
0

Working Swift 2.2 version as two extensions:

extension String {
    public var fourCharCode:FourCharCode {
        var string = self
        if unicodeScalars.count < 4 {
            string = self + "    "
        }
        string = string.substringToIndex(string.startIndex.advancedBy(4))

        var res:FourCharCode = 0
        for unicodeScalar in string.unicodeScalars {
            res = (res << 8) + (FourCharCode(unicodeScalar) & 255)
        }

        return res
    }
}

extension FourCharCode {
    public var string:String {
        var res = String (UnicodeScalar((self >> 24) & 255))
        res.append(UnicodeScalar((self >> 16) & 255))
        res.append(UnicodeScalar((self >> 8) & 255))
        res.append(UnicodeScalar(self & 255))
        return res
    }
}
Klaas
  • 22,394
  • 11
  • 96
  • 107
0

Here's the Swift 3 version of j.s.com's code, cleaned up to eliminate repetition:

func debugPrintFourCcCode(_ value: Int) {
  var res = ""

  if value <= 0x28 {
    res = "\(value)"
  } else {
    func add(_ pos: Int) {
      res.append(String(UnicodeScalar((value >> pos) & 255)!))
    }

    add(24)
    add(16)
    add(8)
    add(0)
  }

  NSLog("Code: \(res)")
}
Kartick Vaddadi
  • 4,818
  • 6
  • 39
  • 55
0

A version for the Mac, but not iOS:

extension String {

    init(_ fourCharCode: FourCharCode) { // or `OSType`, or `UInt32`
        self = NSFileTypeForHFSTypeCode(fourCharCode).trimmingCharacters(in: CharacterSet(charactersIn: "'"))
    }

}
pkamb
  • 33,281
  • 23
  • 160
  • 191
0

Tricksy Swift version:

func convert(fileType: OSType) -> String {
    let chars = [24, 16, 8, 0].map { Character(UnicodeScalar(UInt8(fileType >> $0 & 0xFF)))}
    return String(chars)
}

I've tested on Swift 5, but should work on previous Swift versions.

Nick Ager
  • 1,227
  • 14
  • 15
0

This is a variation of my preferred answer above (thanks pawneebill) I save the creation and deallocation of an NSData intermediate object by using a different NSString initializer.

#import <Cocoa/Cocoa.h>
#import <CoreServices/CoreServices.h> // for EndianU32_NtoB
@implementation NSString (FourCC)

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    NSString * result = nil;
    cccc = EndianU32_NtoB(cccc); // convert to network byte order if needed
    result = [[NSString alloc] initWithBytes: &cccc length: sizeof(cccc) encoding: NSMacOSRomanStringEncoding]; // lossless 8-bit encoding
    return result;
}

Or, more compact but less explained...

+ (NSString *) stringFromFourCC: (OSType) cccc
{
    cccc = EndianU32_NtoB(cccc); // convert to network byte order if needed
    return [[NSString alloc] initWithBytes: &cccc length: sizeof(cccc) encoding: NSMacOSRomanStringEncoding]; // lossless 8-bit encoding
}
Motti Shneor
  • 2,095
  • 1
  • 18
  • 24