From: brodskye at cae.wisc.edu Newsgroups: alt.sb.programmer,rec.games.programmer Subject: Re: HELP - Mixing sample to a different pitch. Date: 18 Mar 1997 17:48:12 GMT Lines: 134 Message-ID: <5gmkgs$2h3m@news.doit.wisc.edu> References: <01bc3362$e451a9e0$11b19dcc@P166> Reply-To: ebrodsky@pobox.com In article <01bc3362$e451a9e0$11b19dcc@P166>, Shawn Odekirk wrote: >I know the subject of sample mixing and avoiding cliping just went through >the newsgroups, but I am still unsure of the best way to mix a sample at a >different pitch. >In order to play back a sample at a pitch different than it was recorded at >I need to duplicate or skip individual samples. I have no problem with >normal pointer operations, but when it comes to non-integral pointer >incrementing, I'm not sure of the most efficient method. >I can cast a double into a pointer that points to my wave data, >incrementing the double by some fractional value, which basically works, >but someone said to use fixed-point and not floating-point math. I've used several methods. I'd recommend starting out by writing a program that takes a wave file and converts the pitch. This avoids all the constraints of real-time programming and makes it much easier to get it working initially and to try out new techniques. Using doubles incremented by a fractional value and casted to an integer for addressing works fine, although it probably won't work in real-time. Once you have it working with the double technique, your test program will be a good framework for testing better methods. >What is the most efficient way to do this in C? The best way I've found is to use fixed point. If you're just doing normal mixing, you have a variable ofs that tells the current offset in units of one: each time that variable is incremented, you're handling the next sample. Instead of considering it to be units of one, consider it to be units of 1/256. Each time it is incremented, you move forward 1/256 of a sample, so it must be incremented 256 times to advance one sample. Now, instead of adding one each time, you add a value appropriate for the frequency: if you want to keep the frequency the same, add 256 each time, to double it, add 512, to half it, add 128. When you want to actually address the sample for mixing or playback, you divide the offset by 256 using something along the lines of sample[ofs >> 8], and you have the sample. This fixed point is sort of analogous to counting money. If you had a variable in the units of dollars, you can only handle dollars. If you add 1, you have one more dollar. However, if you have a bunch of quarters, integer math will fail, as you can add 0.25 to an integer all day and it will remain the same. If you were instead to keep track of the number of cents you have, you would have to add 100 for each dollar and 25 for each quarter, but, at the end of the day, when you want to know rougly how much money you have, you just divide by 100, chop off any fractional part (change doesn't matter to rich programmers;-), and that's the number of dollars. (If fixed point already made sense before you read my analogy, I hope I didn't just confuse you) >How do you do this in assembly language? In assembly language, the sample principle is used, although I frequently implement it differently. Since I usually use 8-bits for the fixed-point fractional part, it is suited for a byte-sized register. I usually put the integer part in a 32-bit register and the fractional part in an 8-bit register, do the sample for the integral and fractional parts of the delta constant, and add, propagating the carry from the fractional part to the integral part. It looks something like this: mov ecx, count mov esi, curptr_int mov al, curptr_frac mov ebx, delta_int ; 32-bits not necessary if your short on regs mov ah, delta_frac loop: add al, ah ; add fractional part adc esi, ebx ; add integral part w/carry from fractional mov dl, [esi] ; the current sample is in dl, process it dec ecx jnz loop mov curptr_int, esi mov curptr_frac, al ; very important, or you'll get distortion This is the method I use, and I'm sure there are several others. One I've heard about frequently is to keep the fractional part of the offset in the high 8-bits of a register. Then, when you add in the delta (also stored in the same format), you only need to do an end-around carry from the top of the fractional part to the bottom of the integral part. The end-around carry can be done with an "adc reg, 0". I don't see much of an improvement there, but, if you can find a way to keep the carry flag around until the next increment, you can eliminate the extra add. Then you'd just have one adc per sample. Of course, you still need to do something for addressing. If you're willing to limit yourself to 16-bit addresses, you could just use the lower half of the register, but for 32-bit addressing, you're going to need to do an "and reg, 0x00FFFFFF" or something like that to mask off the fractional part. I've never tried this method, but it should work and must have some benefits, as I see it mentioned from time to time. >Can anyone recommend any books on sample mixing and filtering to acheive >the best sound? I haven't seen this in any books, so I can't help you there. The only technique I've used for making this sound better is intepolation. Instead of choosing either one sample of the next, you find some way to approximate what the value is as some intermediate point. The easiest way to do this is to use linear interpolation. If you have an array called sample[] and a 8-bit fractional fixed point counter, you can interpolate like this: cursample = (sample[i>>8]*(256 - i%256) + sample[i>>8+1]*(i%256)) >> 9; (although I've argued against using the add-and-divide algorithm for mixing, I believe it is appropriate in this case ;-) This will take a weighted average of the two samples surrounding the current pointer. I've tried this in my test program and the quality didn't seem much better, but stuff I work with is usually sampled at low rates, 8-bit, and isn't that high-quality to begin with. If you're not happy with linear interpolation, then you need to move to something more complicated. I've never tried it, but what you have to do is look at samples around the area around the current one to find some kind of pattern. Ideally, you'd be able to write an equation for the whole wave that would continously describe its behavior at every point in between all the samples, but that would be prohibitively hard for something this simple. For something simpler, I'd probably try forming a parabola with the three samples nearest to the current point (Simpson's formula uses a similar technique to find the area under a curve) and using that equation to approximate the intermediate value. This could probably be done in real-time, but I don't know whether it would be worth the trouble. I had actually never even though about anything beyond simple linear interpolation before writing this response. >Thanks in advance and sorry if everyone is getting tired of hearing the >same questions. Actually, I don't remember ever seeing this question here. I'm sure a lot of people wonder about it, as I frequently get the question by email, but I usually write a short explanation each time. I'll be saving this response in my archives so I can use it next time. Thanks for asking, I enjoyed responding, Ethan Brodsky -- ----