Old 07-10-2019, 06:26 AM   #1
Sju
Human being with feelings
 
Join Date: Jun 2015
Posts: 686
Default Simple real time pitch-down plugin via SRC and interpolation?

Preface: when doing (non-musical) sound design it's often useful to pitch stuff down to achieve all kinds of neat effects. Often the best sounding way to do this is to not actually use any conventional advanced algorithm which stretches the sample back to original length, but just do the resample part to pitch it down.

So, it would be really useful to be do this in real time without having to have to render the audio first and then putting it into a sampler or stretching it in the arrange. But of course there's the problem of buffering.

The idea for a plugin is that it would use MIDI input to start/stop the resampled playback of incoming audio:

Note-on -> start capturing the buffer and play it back slower, interpolating between the captured samples.

Note-off -> Stop playback and reset the buffer.

This seems pretty straightforward to me, but I've really no experience in DSP. I tried going through the built-in JSFX for examples, but couldn't find really anything related to SRC and pitch, just some granular pitch stuff which probably isn't helpful here. I would greatly appreciate it if you JSFX gurus could provide any help with this!
Sju is offline   Reply With Quote
Old 07-10-2019, 03:14 PM   #2
Xenakios
Human being with feelings
 
Xenakios's Avatar
 
Join Date: Feb 2007
Location: Oulu, Finland
Posts: 7,713
Default

JSFX don't have access to the more advanced resamplers that are built into Reaper. So you would need to do something like linear interpolation yourself. More complicated resamplers would probably get too painful to implement in JS. The downside of linear interpolation is that it is quite low quality and the sound quality degradation can easily get audible when using it for pitch shifting.
__________________
For info on SWS Reaper extension plugin (including Xenakios' previous extension/actions) :
http://www.sws-extension.org/
https://github.com/Jeff0S/sws
--
Xenakios blog (about HourGlass, Paul(X)Stretch and λ) :
http://xenakios.wordpress.com/
Xenakios is offline   Reply With Quote
Old 07-10-2019, 03:45 PM   #3
sai'ke
Human being with feelings
 
sai'ke's Avatar
 
Join Date: Aug 2009
Location: NL
Posts: 623
Default

I mean, in JSFX it may be possible to do something naive like oversample a lot (4x or 8x) and then use linear interpolation in that domain and then downsample again. Costly though. There are probably better ways

I mean, if it's just for testing what works, just lerp without oversampling might already be good enough. Then you can render it with proper pitching once you are happy with it.
__________________
[Tracker Plugin: Thread|Github|Reapack] | [Routing Plugin: Thread|Reapack] | [Filther: Thread|Github|Reapack] | [More JSFX: Thread|Reapack]
sai'ke is online now   Reply With Quote
Old 07-11-2019, 02:17 AM   #4
geraintluff
Human being with feelings
 
geraintluff's Avatar
 
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 299
Default

Here's a version that uses linear interpolation (which is not great, and if you're using it for real projects, give me a shout and I'll do something better).

It's monophonic, using only the most recent note. If a new note is triggered before the release of the previous one has finished, that counts as a legato, and it will continue to use the buffer, sliding between notes.

Pitch is determined by MIDI note relative to the "reference note", which defaults to Middle C. If you have slowed down (so you have a buffer that's growing), using legato you can play back faster than real-time using notes above Middle C, but when the buffer is empty it will snap back to realtime.

Pitch-bend is currently not supported, but could be if that's important to you. Polyphony would be a bit more fuss. If anyone else feels like adding better interpolation, go for it - even a 10-tap windowed sinc would be an improvement (probably fine to just ignore the latency, the attack cross-fade should make it OK).

Code:
desc:Live Downsampler (linear interpolation, monophonic)

slider1:reference_note=60<0,127,1>reference note
slider2:sampler_attack_ms=1<0.1,50,0.1>attack (ms)
slider3:sampler_release_ms=5<0.1,50,0.1>release (ms)
slider4:sampler_slide_ms=10<1,100,0.1>slide (ms)

@init

freemem = 0;

sample_buffer = freemem;
sample_write_index = 0;
sample_read_index = 0;

sampler_note = -1;
sampler_speed = 1;
sampler_target_speed = 1;

sampler_fade = 0;

@block

sampler_attack_samples = srate*0.001*sampler_attack_ms;
sampler_attack_step = 1/sampler_attack_samples;
sampler_release_samples = srate*0.001*sampler_release_ms;
sampler_release_step = 1/sampler_release_samples;
sampler_slide_samples = srate*0.001*sampler_slide_ms;
sampler_slide_factor = 1 - exp(-1/sampler_slide_samples);

while (midirecv(midi_offset, midi_msg1, midi_msg2, midi_msg3)) (
  midi_type = midi_msg1>>4;
  midi_channel = midi_msg1&0x0f;
  (midi_type == 0x9 && midi_msg3 != 0) ? (
    sampler_target_speed = pow(2, (midi_msg2 - reference_note)/12);
    
    sampler_fade == 0 ? (
      sampler_speed = sampler_target_speed;
      sample_write_index = 0;
      sample_read_index = 0;
    );
    sampler_note = midi_msg2;
  ) : (midi_type == $x8 || (midi_type == $x9 && midi_msg3 == 0)) ? (
    sampler_note = -1;
  );
);

@sample

sampler_speed += (sampler_target_speed - sampler_speed)*sampler_slide_factor;

// Fade in/out
sampler_note < 0 ? (
  sampler_fade = max(0, sampler_fade - sampler_release_step);
) : (
  sampler_fade = min(1, sampler_fade + sampler_attack_step);
);

sampler_fade > 0 ? (
  sample_buffer[sample_write_index*2] = spl0;
  sample_buffer[sample_write_index*2 + 1] = spl1;

  // Linear sample interpolation
  index0 = floor(sample_read_index);
  ratio = sample_read_index - index0;
  l0 = sample_buffer[index0*2];
  l1 = sample_buffer[(index0 + 1)*2];
  spl0 += (l0 + (l1 - l0)*ratio - spl0)*sampler_fade;
  
  r0 = sample_buffer[index0*2 + 1];
  r1 = sample_buffer[(index0 + 1)*2 + 1];
  spl1 += (r0 + (r1 - r0)*ratio - spl1)*sampler_fade;

  sample_write_index += 1;
  sample_read_index = min(sample_write_index, sample_read_index + sampler_speed);  
);
If you want it to always slide down from real-time (e.g. to make a tape-stop effect), change "sampler_speed = sampler_target_speed;" to "sampler_speed = 1;", and increase the "sampler_slide_ms" slider range up from 100.

Geraint

Last edited by geraintluff; 07-11-2019 at 02:44 AM.
geraintluff is online now   Reply With Quote
Old 07-11-2019, 02:58 AM   #5
sai'ke
Human being with feelings
 
sai'ke's Avatar
 
Join Date: Aug 2009
Location: NL
Posts: 623
Default

One could probably do a pretty fast sinc window on the fly using the following recurrence relation (avoiding many transcendentals):

The following can do the cosine window for instance.

Once to initialize:
Code:
w = 2*$pi/N;
ip = -.5*$pi;
    
y0 = 0;
b1 = 2.0 * cos(w);
y1 = sin( ip - w );
y2 = sin( ip - 2.0*w );
Then per tap:
Code:
y0 = b1 * y1 - y2;
y2 = y1;
y1 = y0;
out = .5*(y0+1);
I use this thing a lot.
__________________
[Tracker Plugin: Thread|Github|Reapack] | [Routing Plugin: Thread|Reapack] | [Filther: Thread|Github|Reapack] | [More JSFX: Thread|Reapack]
sai'ke is online now   Reply With Quote
Old 07-11-2019, 07:55 AM   #6
Sju
Human being with feelings
 
Join Date: Jun 2015
Posts: 686
Default

Thank you so much for the answers and example code! Haha wow Geraint you went above and beyond what I was thinking originally, that's really cool

Quote:
Originally Posted by sai'ke View Post
I mean, if it's just for testing what works, just lerp without oversampling might already be good enough. Then you can render it with proper pitching once you are happy with it.
Yep I was mostly thinking along the same line, to use it as a monitor effect of sorts. Just to get an idea how the final pitched version will sound like while tweaking parameters of the VSTi.

Quote:
Originally Posted by geraintluff View Post
Here's a version that uses linear interpolation (which is not great, and if you're using it for real projects, give me a shout and I'll do something better).

If you want it to always slide down from real-time (e.g. to make a tape-stop effect), change "sampler_speed = sampler_target_speed;" to "sampler_speed = 1;", and increase the "sampler_slide_ms" slider range up from 100.

Geraint
Thank you so much! I tested it against REAPER's own interpolation (using 192pt Sinc) with a metallic impact sound, and it actually did quite well! There's some noticeable darkening/muffling in the plain lerp, but it's not really a problem. The pitched down layer will always be mixed in with a bunch of other stuff, so I think the sound quality difference would rarely be audible.

Regarding tape stop and sliding, I don't really intend to use this as a playable "instrument" at all, but instead for pitching down single hit samples like impact sounds. On the other hand, it might actually sound good to have for example a fast attack envelope doing a pitch down at the beginning, for some extra impact.

Will have to play around with the code; I think I'll be able to figure it out from here with the excellent example

Cheers!
Sju is offline   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 08:50 AM.


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