Old 01-19-2020, 11:31 PM   #1
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default A Basic Delay Plugin Explained

I got a request from a fellow user about how to make a basic delay plugin. I figured I would make it a public post so it was open for feedback if anyone else wanted to chime in. This first post will have the full source and the second post will break it down by sections and explain what is happening where.

The purpose is a basic short delay that can create echoes through feedback. I'm not going to claim to be the best teacher or explain the best and I can easily talk too much, but hopefully the user who asked will benefit anyways.

++changelog

2020/01/20 ... incorporated Ashcat's moving the feedback into the function itself, added db2ratio()

Also, I should point out that I put the default values to those exact settings because I liked how it sounded on Otaku Gang's "Ten Crack Commandments (Star Wars Remix)" which is the track I tend to test my plugins out on. And now you know.

Code:
desc:simple delay plugin

slider1:350<1,500,1>Delay (ms)
slider2:-15<-90,0,0.1>Wet (dB)
slider3:-45<-90,0,0.1>Feedback (dB)

@init

buf0 = 100000;
buf1 = 200000;

function db2ratio(db) ( 10^(db * 0.05); );

function delay_set(buffer,ms,fbGain)
 instance(buf,size,frac,pos)
(
  buf = buffer;
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
  feedbackGain = db2ratio(fbGain);
);

function delay(in)
 instance(pos,buf,old,out,size,frac,feedbackGain)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in + (out * feedbackGain);
  pos += 1;
  pos >= size ? pos -= size;
  out;
);

@slider

delay0.delay_set(buf0,slider1,slider3);
delay1.delay_set(buf1,slider1,slider3);

wetGain = db2ratio(slider2);


@sample

spl0 += delay0.delay(spl0) * wetGain;
spl1 += delay1.delay(spl1) * wetGain;

Last edited by SaulT; 01-20-2020 at 09:04 PM.
SaulT is offline   Reply With Quote
Old 01-19-2020, 11:32 PM   #2
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default

Code:
desc:simple delay plugin

slider1:350<1,500,1>Delay (ms)
slider2:-15<-90,0,0.1>Wet (dB)
slider3:-90<-90,0,0.1>Feedback (dB)
What controls do we want? This is a delay so it seems pretty straightforward - we want to select how long the delay is, and milliseconds is a fairly intuitive pick for units. I'm picking a simple volume control for how much delay to blend back in, in decibels. Similarly, I'm picking decibels for feedback. Audio people should know decibels, it's one of the basic units of measure. 90 decibels of attenuation is basically silence for all intents and purposes, so that's a good minimum.

Code:
@init

buf0 = 100000;
buf1 = 200000;
Okay, so I could have said something like "buf0 = 0; buf1 = buf0 + srate;" but instead I'm picking a predetermined range. Why is buf0 100k? Because I'm thinking of the worst case scenario - someone is in a 96 kHz samplerate and they pick a 1 second delay. We want our plugins to work anywhere from 22.05 kHz to 96 kHz samplerates, and what if we are trying to create a reverb with dozens of delay lines? We want to think ahead and plan for these things.

Code:
function delay_set(buffer,ms)
 instance(buf,size,frac,pos)
(
  buf = buffer;
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
);
Every time we change a slider we are changing something about the delay. One of the values of using functions is that instead of putting all of this in the @slider section we can put all of the variable tweaking in a separate function. Secondary benefit? By putting it in a function we can reuse the code for multiple delay lines. Remember, we are coding for two channels, left and right, right? Now, what if we wanted to add support for more channels? It's relatively simple, we create delay2 and so forth and call delay2.delay_set(...). It isn't full blown object oriented code, but using functions to create objects makes our code simpler and quicker to write!

Code:
 instance(buf,size,frac,pos)
when a variable calls this function it becomes an object and JS assigns these variables to that object. So delay0 will now have delay0.buf, delay0.size, etc.

Code:
  size = srate * ms * 0.001;
  frac = size - floor(size);
  size = floor(size);
Remember that we get milliseconds by dividing the samplerate (samples per second) by 1000. This is the same as multiplying by 0.001. The tricky part of this code is that if we want a precise delay often we will end up with a fractional value. For example, at 44100 kHz samplerate if we want a 35 ms delay then the exact value of that delay is 0.035 * 44100 = 1543.5 samples. We can't have half a byte of buffer so we use a technique called linear interpolation to get that fractional delay amount. I love interpolation, but for a basic delay plugin we really don't need anything fancier than linear interpolation.

Code:
function delay(in)
 instance(pos,buf,old,out,size,frac)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in;
  pos += 1;
  pos >= size ? pos -= size;
  out;
);
The heart of our plugin here. We are using a circular buffer to create our delay. This means that we are cycling through a buffer with a position variable keeping track of our place. So we pull out the current value, we linear interpolate the precise value, and put it aside for a second. Our new sample is put into the buffer, we move the position variable up one, and if it is greater than the size it goes back to the beginning. The last value we list in a function is what is returned, in javascript or whatnot we would specifically say return out;, here the return is assumed.

Code:
@slider

delay0.delay_set(buf0,slider1);
delay1.delay_set(buf1,slider1);

wetGain = 10^(slider2/20);
feedbackGain = 10^(slider3/20);
See how little code we need here? We update the values of our delays and we calculate our decibel multipliers. Remember, decibels are just ratios and volume adjustment is just a multiply.

Code:
@sample

spl0 += delay0.delay(spl0 + old0 * feedbackGain) * wetGain;
spl1 += delay1.delay(spl1 + old1 * feedbackGain) * wetGain;

old0 = spl0;
old1 = spl1;
This part of the code is executed every sample, right? So each sample we are putting the current sample and some of the previous output into our buffer, then make sure we save the current output for use next sample. Hopefully this is clear enough.

...

And, umm, that's basically it. Now, there are a lot of places you could go with this. You can take this technique with the circular buffers and make reverb functions, or you could put a filter on the delayed output, all kinds of things.

Remember that Reaper lists all of the current variables in the "development environment" window. You can check to make sure they look right. A common mistake I make when I'm writing is I'll forget to put a function's variable inside the instance() command. If I was to see the pos variable just hanging out but not delay0.pos and delay1.pos, then I'll know I forgot to put it in the instance().

I could have written the plugin without the functions, but it's such a valuable and important part of JS that I really needed to make sure that I illustrated it. Using functions makes your code a lot simpler to write, easier to read, and much more scalable.

Last edited by SaulT; 01-20-2020 at 12:23 AM.
SaulT is offline   Reply With Quote
Old 01-20-2020, 05:19 AM   #3
lgz
Human being with feelings
 
Join Date: Jan 2020
Posts: 4
Default best delay code

It's great that I saw it!! This post is really helpful. I learned a lot in the interpretation of each line of code!
lgz is offline   Reply With Quote
Old 01-20-2020, 09:27 AM   #4
nofish
Human being with feelings
 
nofish's Avatar
 
Join Date: Oct 2007
Location: home is where the heart is
Posts: 9,516
Default

I appreciate your '... explained' posts (also this one), thanks.
nofish is online now   Reply With Quote
Old 01-20-2020, 10:14 AM   #5
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

db2ratio is such a ubiquitous function, and with all your talk about how useful functions are, you left it out. I’m not sure it would make the @slider section a lot tidier, but if we’re illustrating how functions save us from writing redundant code...

But wait! One big thing and then a slightly smaller thing.

1) We don’t want to feed the mix signal back through the delay at the end there. The feedback split should come before the mixer. In fact, it should probably happen in the delay function itself.

Code:
function delay(in)
 instance(pos,buf,old,out,size,frac)
(
  out = buf[pos];
  out = out*(1-frac) + old*frac;
  old = buf[pos];
  buf[pos] = in + (out * feedbackGain);
  pos += 1;
  pos >= size ? pos -= size;
  out;
);
Then in @sample you just send it the input and remove all references to old0 and old1.


The other thing I wanted to mention is more about the fundamental nature of this very simple type of delay. Basically, it’s gonna glitch hard and strange when you adjust the delay time. Consider what happens when we have a long time and then shorten it. We basically leave a bunch of buffer slots full of stuff, it never actually plays out so we kind of jump back to even further back then we already were, and then if we switch back to a longer time we’ll end up hearing stuff that maybe happened quite a while ago. We expect weird things when simple digital delays are modulated, and I understand that this example wasn’t meant to address that, but just pointing it out for those who might have higher expectations.

Last edited by ashcat_lt; 01-20-2020 at 10:42 AM.
ashcat_lt is online now   Reply With Quote
Old 01-20-2020, 08:59 PM   #6
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default

Quote:
1) We donít want to feed the mix signal back through the delay at the end there. The feedback split should come before the mixer. In fact, it should probably happen in the delay function itself.
And this, friends, is why I made this a public post. I went through an iteration or two and for some reason the code just seemed weird to me. Ashcat is of course correct and this is the more proper way to do it. I'll update the code above with a changelog.

...also, for completeness sake, how about I add that db2ratio code.

A very good point, the delay is not going to smoothly modulate necessarily and this function is definitely not programmed to accommodate changes in delay length!
SaulT is offline   Reply With Quote
Old 01-21-2020, 12:31 AM   #7
Tale
Human being with feelings
 
Tale's Avatar
 
Join Date: Jul 2008
Location: The Netherlands
Posts: 3,059
Default

Quote:
Originally Posted by SaulT View Post
Okay, so I could have said something like "buf0 = 0; buf1 = buf0 + srate;" but instead I'm picking a predetermined range. Why is buf0 100k? Because I'm thinking of the worst case scenario - someone is in a 96 kHz samplerate and they pick a 1 second delay. We want our plugins to work anywhere from 22.05 kHz to 96 kHz samplerates, and what if we are trying to create a reverb with dozens of delay lines? We want to think ahead and plan for these things.
You could also have stored the samples interleaved (i.e. 0, 1, 0, 1, 0, 1, ...). That way you don't have to worry about buf0 and buf1 overlapping, even beyond 96 kHz.
Tale is offline   Reply With Quote
Old 01-21-2020, 07:58 AM   #8
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default

Which is a valid point, of course. My thinking was that if someone wanted to either extend the technique to 3+ channels or take the concept to multiple delay lines for reverb or some such it would be more modular. In a different iteration of this type of plugin I downsampled the delay before storing it, opening up delays of potentially 30+ seconds, assuming that JS memory is still limited to 8 megs of course. Itís good that people know there are more than one way to approach the problem.
SaulT is offline   Reply With Quote
Old 01-21-2020, 10:55 AM   #9
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

So the OP kind of assumes that we already know how the memory buffer and ďarraysĒ work in JS. Thatís one of the weirder concepts both for new coders and for those of us with some experience in other languages where there real arrays to work with.

I have this stupid idea about doing the whole thing more like an analog delay where there is just a set number of samples in the buffer and we change delay time by changing how fast we read/write to it. Iím sure somebodyís done it somewhere. It seems like it helps both with modulation and with the fractional sample issue if done correctly. It also seems like it could potentially use lots of CPU.
ashcat_lt is online now   Reply With Quote
Old 01-21-2020, 01:51 PM   #10
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default

Default assumption of 22.05 kHz? If 44.1 or 48 then 2x downsample, if 88 or 96 then 4x downsample, that type of thing? It sounds plausible. A lot of time Iím lowpassing my delays anyways, there is certainly a style where you might not need or want anything above 10 kHz. The positive is more room for delay, the downside is all that up and downsampling... Seems plausible tho.
SaulT is offline   Reply With Quote
Old 01-21-2020, 02:57 PM   #11
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Not exactly what I was saying.

Say for the sake of simplicity that we have a buffer 10 with 10 slots. If we want the delay to be 10 samples long, we read and write to each successive buffer slot on each sample. If we want a delay that is 5 samples long, we read/write every other slot per sample. If we want a 1 sample long delay we do it for every 10th slot. Then we do...something...to fill the slots that skipped for when we change the delay time again.
ashcat_lt is online now   Reply With Quote
Old 02-09-2020, 10:17 AM   #12
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Quote:
Originally Posted by SaulT View Post
Similarly, I'm picking decibels for feedback. Audio people should know decibels, it's one of the basic units of measure. 90 decibels of attenuation is basically silence for all intents and purposes, so that's a good minimum.
I meant to mention this originally too, but caught up in other parts of it. I suppose it’s a little more advanced point than the very basics that you were addressing here, but at this point I think it’s safe to get into it.

In the floating point engine, this kind of assumption is dangerous. You can usually get away with it in analog because -90db is going to bury it in the noise floor, but here we don’t have that safety net. We do, however, have a bunch of ways that we might amplify and smash this signal after it comes back out. One might ask why anyone would add enough gain to hear this and I would answer “guitar amp”.

What I like to do is pick a lower usable limit and then have the slider go 1 step further than that and call that OFF. Like one of those volume controls with the power switch built in.

So like:
Code:
slider3:-45<-91,0,0.1>Feedback (dB, -91 = off)
Then in the delayset function, change the feedback line
Quote:
fbGain == -91 ? feedbackGain = 0 : feedbackGain = db2ratio(fbGain);
It IS discontinuous and could cause clicks going back and forth, but we haven’t bothered smoothing anything else yet, so...
ashcat_lt is online now   Reply With Quote
Old 02-09-2020, 10:58 AM   #13
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Now this is going even further down the road...


But then I look at the other end of that slider. Youíve got it topping out at 0db in order to stop it from self-oscillating with escalating velocity. Thatís a very good idea. Positive feedback here could end up getting REALLY LOUD. Just as the real noise floor is down past -300db, the real clipping ceiling is over 300db louder than your DAC can pass or any fixed point file can reproduce. I do these things on purpose sometimes. It gets stupid loud.

At 0db feedback, if we play something that is shorter than the delay time, it will repeat exactly the same literally forever. Iíve done that too. Turn off the speakers and walk away and come back days later and itís still playing whatever i last did. Thatís cool.

But itís also going to mix in whatever you play next, and whatever you play after that, and the whole thing is going to keep getting louder and louder. Worst case scenario, you play the exact same thing over and over in time with the delay. This will also escalate quickly toward infinity. We have to decide how much we care about that. We can just do like ReaDelay and tell folks ďyeah it does thatĒ, or we could try to handle it ourselves.

The easiest way to stop it getting quite so loud is to just choose a limit and do that min(max()) thing before we write to the buffer. Then itíll just get more distorted as you play. If we want that distortion to sound better, we call our favorite waveshaping function at this point instead. This will still end up getting pretty loud - heading toward square waves with RMS (!!!) levels equal to whatever ceiling weíve set.

If we want it to work much more gracefully, we could take a page from some pedal delays which compress the signal on the way into the buffer and then expands it on the way back out. Now each of those processes are like a whole other thread like this, but itís actually just a couple functions that we call from within our delay function. What ends up happening is that the whole mix gets pushed down as you add new material in a very natural and fairly transparent way. Real pedals also have real limits, and a combination of curvy saturation with compansion is really the best way to go.

Then you could even allow positive feedback for those whackjobs that actually enjoy self-oscillation. Itíll get loud, but it wonít open a portal to the abyss and take the entire known universe with it anymore.

Course then weíre going to want to automate that delay time...
ashcat_lt is online now   Reply With Quote
Old 04-07-2020, 04:44 AM   #14
Hypex
Human being with feelings
 
Join Date: Mar 2015
Location: Australia
Posts: 147
Default

Thanks SaulT for your guide. The code looks different to the JS: Delay. Either way it helps to know the relation between where Reaper is on the FX timeline and what happens in the sample routine. Been a while since I read up on JSFX and some effects do things differently.

Because it's my obsession, I wonder how you go about a stereo delay that bounces each delay across stereo channels? I've examined the JS Delay with the bounce version and I can't see it doing much apart from mixing the sample together, I presume to mono the sound, so that a mono sound gets bounced around. Not exactly a stereo delay. But it's not obvious where it bounces to left or right.

I also find it can be bit harsh, since it pans hard left or right and doesn't bring the bounces together. Nor have any stereo field control. It's not the best on a guitar lick but can be effective on a stereo vocal delay.
Hypex is offline   Reply With Quote
Old 04-08-2020, 11:13 AM   #15
SaulT
Human being with feelings
 
Join Date: Oct 2013
Location: Seattle, WA
Posts: 807
Default

Thanks, there are many ways to skin a cat, as they say.

The simple way to create a stereo delay is to sum to mono then pan that mono signal to different places in the spectrum. Anything more complicated would have to start making assumptions about the incoming signal, I think. I havenít done a lot of thinking about delay lately, itís mostly been about saturation and lo-fi for me, is there something specific that youíre interested in?
SaulT is offline   Reply With Quote
Old 04-08-2020, 11:41 AM   #16
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Ping pong delay really is just sending the feedback from each buffer to the other instead of back to itself. I took a quick look at how to hack this code into doing that, but didnít have a lot of time.
ashcat_lt is online now   Reply With Quote
Old Yesterday, 08:21 AM   #17
Hypex
Human being with feelings
 
Join Date: Mar 2015
Location: Australia
Posts: 147
Default

I suppose what I'm most interested in is a ping pong delay as it's known. I shouldn't confuse terms; as I tend to think of a basic delay as being a mono delay, since it just copies the sound as is; and I think of a stereo delay as bouncing across the speakers in ping pong fashion, even though the delay bounced around is a mono copy. It's just when I listened to a song with an obvious stereo echo effect I would call it a stereo delay, though perhaps dual delay is a better term.


So I've tried a few things. I found a bouncing delay can be good for vocals. I suppose that would be an LCR delay, since I've applied it to a vocal track, and feed back both wet and dry. For vocals this it can be effective with only drums behind it.


I've also done this to a electric guitar chord sustained, and when it ends the echo trail left bouncing sounds good. For comparison, one example would be Billy Idol's Flesh for Fantasy. I recommend the extended mix as it starts with an immediate delay on the guitar that bounces away on the speakers, after the guitar stops.

I also tried it on a small guitar lick. It was fine as a standard delay. But bouncing it just sounded too harsh. For example look up Genesis, Land of Confusion, extended mix. After the guitar is brought in they delay the last note at 0:55 with what sounds like a filtered delay. It's simple but effective. I've tried duplicating the delay and found on a similar lick a 300 ms delay at -6dB feedback is close, just maybe a little faster than that song.


But, it doesn't have the cool bouncing effect either. So it sounds plain by itself or with only drums. However, putting a bounce on it can make it sound too harsh on the panning. This is where I wonder if some kind of LCR delay would be better to fatten up the delay so it has a cool bounce?


Now, back to the code. The problem with EEL is that it can be confusing to read. Having variables whose value points to an internal array location doesn't help for readability. In my own code I've considered to write an Array support function that acted as an allocator, both for memory management and readability. After seeing the code for this delay example it makes more sense than the uncommented code with senseless variable names. A few things stand out:


1. Just an observation. Here ratio is computed with 10^(db * 0.05). Or 10^(dB/20). Reaper JSFX uses 2^ (dB/6). Haven't looked if one is more accurate or the same result as the other.


2. The code here has helped me to see a mix buffer is used. Allowance of 1 second. I see this means all work must be performed in this window. I imagine this could be problematic if the delay goes past the window. It would be easy if the mix buffer was exactly as long as the effect would take, but with a window only then the delay must be calculated to work in that window. In the case the effect tails off longer than the window, quite likely, then a tail from the previous window must be calculated to continue. The code looks to handle this fine.


3. Bouncing ping pong echo effect in JS: Delay. After looking and re-looking I still cannot see how it bounces from one speaker to the other. I understand the idea behind it and myself would implement it by flipping a buffer or buffer index after counting the delay time. I don't see this. I see what looks like taking a mono mixdown multiplied by some right channel sample put in a left channel, and the left channel put in the right, at the same time. This doesn't make any sense! Not to me anyway. Where on earth is it flipping the channels!?


4. I see an immediate problem with bouncing, if the sample is longer than the delay. Then it will cut it off unless it's designed to compensate. I've heard clicks in the tempo synced delay JS Delay Tempo Ping-Pong and I'm sure this was happening the way it sounded. Going just by my ear.


5. I find I don't like the way Reaper uses a set Take FX buffer length. Some delays can keep trailing off and even though they tend to disappear in the mix by themselves they are still there in the mix. A sudden cut off will cause an audible click. I listened to some delays I set up and they cut out. I would prefer they fade out evenly on the tail to avoid clickage. I obviously need to add a soft gate to fade below a threshold. I just prefer not to layer one effect on another to compensate. I like to set up a single delay so it does disappear softly before the next note appears. Just me I guess, prefer to simplify it when I can. :-)

Last edited by Hypex; Yesterday at 06:02 PM.
Hypex is offline   Reply With Quote
Old Yesterday, 09:56 AM   #18
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Quote:
Originally Posted by Hypex View Post
1. Just an observation. Here ratio is computed with 10^(db * 0.05). Or 10^(dB/20). Reaper JSFX uses 2^ (dB/6). Haven't looked if one is more accurate or the same result as the other.
db = 20 log10 (Vout/Vin), so Vout/Vin = 10^(db/20) exactly. 2^(db/6) is an approximation which sets 6 "db" as exactly 2 times gain, which it really shouldn't be. Double gain is actually 6.02...something.


Quote:
2. The code here has helped me to see a mix buffer is used. Allowance of 1 second.
Well there's a space of 100000 slots between the start of the left buffer and the start of the right buffer, so the delay can be 100000/sample rate long before the left buffer starts to "leak into" the right buffer, which will not work the way you want. A lot of the JS delays do a thing where the left and right samples are sort of interleaved into the same buffer such that buffer[0+2x] is the left side and buffer[1+2x] is the right. This way you don't really have to define the end of the buffer, and can get it to be half as long as the 8 million or so slots will allow.


Quote:
3. Bouncing ping pong echo effect in JS: Delay.
You're talking about "Delay w/Tempo Ping-Pong"? Yeah, that's weird. Unless I'm missing something, I think it's more like a mono-summed delay with autopan on the output, which frankly you could just do with a pair of plugins. Not how ping pong delay normally works.





Quote:
4. I see an immediate problem with bouncing, if the sample is longer than the delay.
Again, not the normal way to do it. If you just cross-over the feedback between the buffers, it just naturally makes each consecutive repeat come up on the other side of the stereo field. They go back and forth on each repeat like a real ping pong should.



Quote:
5. I find I don't like the way Reaper uses a set Take FX buffer length.
I never use Take FX. Not necessarily for this reason, but it's another example of how automating Track FX is usually more useful.
ashcat_lt is online now   Reply With Quote
Old Yesterday, 10:38 AM   #19
ashcat_lt
Human being with feelings
 
Join Date: Dec 2012
Posts: 4,679
Default

Trying to make this particular delay ping pong is a bit complicated by a couple of things, and I'm not there yet, but I did notice that it's only actually putting out one repeat no matter what the feedback is set to because the delay_set is not storing feedbackGain as an instance variable, but the delay function is looking for it as an instance variable. Need to add that variable to the instance list in the _set function like so:
Code:
function delay_set(buffer,ms,fbGain)
  instance(buf,size,frac,pos,feedbackGain)
 (   buf = buffer;
     size = srate * ms * 0.001;
     frac = size - floor(size);
     size = floor(size);
     feedbackGain = db2ratio(fbGain); );
ashcat_lt is online now   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 11:24 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2020, vBulletin Solutions Inc.