Sunday, December 5, 2010

Merging Wav Files in Objective C.

On a recent Mac Development project I had to merge two audio inputs into a single file which was an interesting exercise in understanding the rfc wav spec. While concatenation of wav files is simpler since you just have to remove the header from one file and update the header in the other file to reflect the new file size, merging two sounds so that they play simultaneously is a little bit trickier. I've decided to post my objC implementation here in case someone else ever runs into a similar need. Suggestions and comments as usual are always welcome. Especially in regards to clamping the new values uint values to a max which I've ignored in this implementation since it didn't seem to affect the output merged sound I was creating negatively.



    1 -(void)mixSkypeWavFile:(NSString*)wav1 withWavFile:(NSString*)wav2 intoNewWavFile:(NSString*)outWav{
2 //the format of our wav files is ("Subchunk1Size = Hex 12/Dec 18" plus 2 bytes ExtraParamSize hex "0000"
3 //instead of the standard ("Subchunk1Size = Hex 10/Dec 16")
4
5 //a good breakdown for the wave spec bytes https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
6 //turns out the ones here are little endian for numbers and bigendian for strings. go figure.
7
8 //once you understand that you can make sense of this nice comment on stack overflow from lucius
9 //http://stackoverflow.com/questions/1540380/how-to-lower-sound-on-the-iphones-sdk-audioqueue
10 // the strat is to add each 2byte sample together and then divide by two to get the average sound at that time.
11 // this has the adverse effect of lowering the volume too but shouldn't bee to bad since we only need to do this once.
12 NSData * wav1Data = [NSData dataWithContentsOfFile:wav1];
13 NSData * wav2Data = [NSData dataWithContentsOfFile:wav2];
14
15 //grab one of the 46 byte headers from these files (assume they are identical which they should be for this to work.)
16 //grab data from wav1 without headers
17 //grab data from wav2 without headers
18 // you don't have to modify the header if you don't append the files one after the other and change the total file size!
19 // divide each 2byte sample which is just a number then add them together to create the mixed outWav data.
20 //keep in mind these numbers need to be little endian. but at 2 byte blocks little endian and big endian seem to be equiv. (we'll test this now.)
21
22 int wav1DataSize = [wav1Data length] - 46;
23 int wav2DataSize = [wav2Data length] - 46;
24 if (wav1DataSize <= 0 || wav2DataSize <= 0) {
25 NSLog(@"error merging wav sources for now just error out of sound mixing.");
26 [[NSAlert alertWithMessageText:@"error merging sound sources" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"We apologize, please check for updates and contact support if this continues to be an issue."] runModal];
27 return;
28 }
29
30 //Use the header from the shorter of the two files.
31 NSMutableData * headerBuffer;
32 int outputWavDataSize;
33 if (wav1DataSize < wav2DataSize) {
34 outputWavDataSize = wav1DataSize;
35 headerBuffer = [NSMutableData dataWithData:[wav1Data subdataWithRange:NSMakeRange(0, 46)]];
36 }else{
37 outputWavDataSize = wav2DataSize;
38 headerBuffer = [NSMutableData dataWithData:[wav2Data subdataWithRange:NSMakeRange(0, 46)]];
39 }
40
41 // NSLog(@"are both headers identical? = %d", [headerBuffer isEqualToData:headerBuffer2]);
42 // NSLog(@"header is = %@",[headerBuffer description]);
43
44 //read each 2 byte increment
45 //get an average
46 //store in a merged buffer
47 NSData * wav1DataBuffer1;
48 NSData * wav1DataBuffer2;
49 NSData * wav2DataBuffer1;
50 NSData * wav2DataBuffer2;
51
52 NSMutableData * littleEndianHexWav1Sample;
53 short iValueWav1Sample;
54 NSMutableData * littleEndianHexWav2Sample;
55 short iValueWav2Sample;
56 short iValueWavSampleAverage;
57
58 for (int i=0; i<(outputWavDataSize/2); i++) {
59 //simulate little endian by flipping the bytes
60 wav1DataBuffer1 = [wav1Data subdataWithRange:NSMakeRange(46 + (i*2), 1)];
61 wav2DataBuffer1 = [wav2Data subdataWithRange:NSMakeRange(46 + (i*2), 1)];
62 wav1DataBuffer2 = [wav1Data subdataWithRange:NSMakeRange(46 + (i*2) + 1, 1)];
63 wav2DataBuffer2 = [wav2Data subdataWithRange:NSMakeRange(46 + (i*2) + 1, 1)];
64 //littleEndianHexWav1Sample = [[[wav1DataBuffer2 description] substringWithRange:NSMakeRange(1, 2)] stringByAppendingString:[[wav1DataBuffer1 description] substringWithRange:NSMakeRange(1, 2)]];
65 // littleEndianHexWav2Sample = [[[wav2DataBuffer2 description] substringWithRange:NSMakeRange(1, 2)] stringByAppendingString:[[wav2DataBuffer1 description] substringWithRange:NSMakeRange(1, 2)]];
66 littleEndianHexWav1Sample = [NSMutableData dataWithData:wav1DataBuffer1];
67 [littleEndianHexWav1Sample appendData:[NSMutableData dataWithData:wav1DataBuffer2]];
68 littleEndianHexWav2Sample = [NSMutableData dataWithData:wav2DataBuffer1];
69 [littleEndianHexWav2Sample appendData:wav2DataBuffer2];
70
71 // [littleEndianHexWav1Sample getBytes:&iValueWav1Sample length:2];
72 // [littleEndianHexWav2Sample getBytes:&iValueWav2Sample length:2];
73 NSString* wav1HexString = [[littleEndianHexWav1Sample description] substringWithRange:NSMakeRange(1, 4)];
74 NSString* wav2HexString = [[littleEndianHexWav2Sample description] substringWithRange:NSMakeRange(1, 4)];
75 unsigned wav1Hexint;
76 unsigned wav2Hexint;
77
78 [[NSScanner scannerWithString:wav1HexString] scanHexInt:&wav1Hexint];
79 [[NSScanner scannerWithString:wav2HexString] scanHexInt:&wav2Hexint];
80 unsigned wavAverage;
81 //wavAverage = ((wav1Hexint/sqrt(2))+(wav2Hexint/sqrt(2)));
82 // wavAverage = (pow(wav1Hexint, 2)+pow(wav2Hexint,2))/sqrt(2);
83 // wavAverage = (wav1Hexint+wav2Hexint)/2;
84 wavAverage = (wav1Hexint+wav2Hexint);
85 iValueWav1Sample = wav1Hexint;
86 iValueWav2Sample = wav2Hexint;
87 iValueWavSampleAverage = wavAverage;
88 // NSLog(@"wav1 bytes = %@, intVal = %d",littleEndianHexWav1Sample, iValueWav1Sample );
89 // NSLog(@"wav2 bytes = %@, intVal = %d",littleEndianHexWav2Sample, iValueWav2Sample );
90 // iValueWavSampleAverage = (iValueWav1Sample/2) + (iValueWav2Sample/2);
91
92 //http://stackoverflow.com/questions/836681/iphone-int-to-nsdata
93 NSData * bigEndian = [NSData dataWithBytes:&iValueWavSampleAverage length:2];
94 NSMutableData * littleEndian = [NSMutableData dataWithData:[bigEndian subdataWithRange:NSMakeRange(1, 1)]];
95 [littleEndian appendData:[bigEndian subdataWithRange:NSMakeRange(0, 1)]];
96 //NSString * littleEndian = [[bigEndian substringFromIndex:[bigEndian length]/2] stringByAppendingString:[bigEndian substringToIndex:[bigEndian length]/2]];
97 // NSLog(@"the average val = %d, which becomes little endian %@", iValueWavSampleAverage, [littleEndian description]);
98
99
100 // char temp[lengthOfMessage];
101 // strcpy(temp, [littleEndian cString]);
102 [headerBuffer appendData:littleEndian];
103 //
104 //
105 // NSLog(@"header is = %@",[headerBuffer description]);
106
107 }
108 // NSLog(@"header is = %@",[headerBuffer description]);
109 [headerBuffer writeToURL:[NSURL fileURLWithPath:outWav] atomically:YES];
110 }
111
112

4 comments:

  1. Hi..Thamster ..dats a very gud tutorial..
    Can you please explain how can I create a new wav file by appending two wav files one after the another..i.e. without mixing them both.

    ReplyDelete
  2. Sure I actually posted a generic method for both, concatenation and mixing on StackOverFlow, I figured more people would see it there.

    http://stackoverflow.com/questions/2578862/how-do-i-combine-merge-two-wav-files-into-one-wav-file

    This the part you are interested in:

    If you work with the bytes of a wav file directly you can use the same strategy in any programming language. For this example I'll assume the two source files have the same bitrate/numchannels and are the same length/size. (if not you can probably edit them before starting the merge).

    First look over the wav specificaiton, I found a good one at a stanford course website:
    https://ccrma.stanford.edu/courses/422/projects/WaveFormat/

    Common header lengths are 44 or 46 bytes.

    If you want to concatenate two files (ie play one wav then the other in a single file):

    find out what format your wav files are
    chop off the first 44/46 bytes which are the headers, the remainder of the file is the data
    create a new file and stick one of the headers in that.

    new wav file = {header} = {44/46} bytes long

    add the two data parts from the original files

    new wav file = {header + data1 + data2 } = {44/46 + size(data1) + size(data2)} bytes long

    modify your header in two places to reflect the new file's length.

    a. modify bytes 4+4 (ie. 4 bytes starting at offset 4). The new value should be a hexadecimal number representing the size of the new wav file in bytes {44/46 + size(data1) + size(data2)} - 8bytes.

    b. modify bytes 40+4 or 42+4 (the 4 bytes starting at offset 40 or 42, depending on if you have a 44byte header or 46 byte header). The new value should be a hexadecimal number representing the total size of the new wav file. ie {44/46 + size(data1) + size(data2)}

    ReplyDelete
  3. Do you have any example code for appending 2 wav files together? I have tried what you said in the last comment, but it wont work at all. I think i did something wrong.

    Regards

    ReplyDelete
  4. could you please tell me how to concatenate two wav files instead of mixing or merging.

    ReplyDelete