Let's start with the block diagram. A recursive delay consist of forward path, a feedback path, a bulk delay and a feedback gain. There are four different way to arrange these.
For each we can derive the coupled difference equations
$$ \begin{cases}
\text{I:} & y[n] = g \cdot v[n-N] & v[n] = y[n] + x[n] \\
\text{II:} & y[n] = v[n-N] & v[n] = g \cdot y[n] + x[n] \\
\text{III:} & v[n] = y[n-N] & y[n] = g \cdot (v[n] + x[n]) \\
\text{IV:} & v[n] = g \cdot y[n-N] & y[n] = v[n] + x[n] \\
\end{cases} \tag{1} $$
They all behave slightly different behaviors (which have pros and cons) but
I think you are trying to implement version II so that's the equation we need to implement.
Turns out delays seem easy but are actually a bit tricky to implement because the state management is hard to do efficiently. The cleanest way is to use a circular buffer with write pointer and a read pointer. The read pointer trails the write pointer by the amount of the desired delay. New data gets written to the write pointer location, the delayed pointer get read from the read pointer location and than both pointers get incremented module the delay buffer size.
The modulo function is quite inefficient no most processors. A partial workaround is to make the delay buffer size a power of 2 and than use a single bit and operation to mask out the overflowing bit.
Here is some code that implements. It's poor code quality but I was trying to stick with your initial flow.
import numpy as np
import scipy.io.wavfile as wavfile
DELAY = 0.01 # seconds
WET = 0.50
FEEDBACK = 0.8
BLOCK_SIZE = 1024 # 23 ms @ 44.1 Khz
MAX_DELAY_LOG2 = 12 # max delay is a power of 2
MAX_DELAY = 1 << MAX_DELAY_LOG2
MAX_DELAY_MASK = MAX_DELAY - 1 # mask for circular buffer
sr, x = wavfile.read("input.wav")
y = np.zeros_like(x)
delayBuf = np.zeros(MAX_DELAY) # delay buffer
delayWP = 0 # write pointer
delayRP = MAX_DELAY - int(DELAY * sr) # read pointer
def process_block(inp):
global delayBuf
global delayWP
global delayRP
global MAX_DELAY_MASK
global FEEDBACK
out = np.zeros_like(inp)
# add the delayed output
for i in range(0, BLOCK_SIZE):
out[i] = delayBuf[delayRP]
delayBuf[delayWP] = out[i] * FEEDBACK + inp[i]
# update the delay pointers circularly
delayWP = (delayWP + 1) & MAX_DELAY_MASK
delayRP = (delayRP + 1) & MAX_DELAY_MASK
return out
# run the block by block loop
for i in range(0, len(x), BLOCK_SIZE):
if i + BLOCK_SIZE > len(x):
break
inp = x[i:i + BLOCK_SIZE]
y[i:i + BLOCK_SIZE] = (1 - WET) * inp + WET * process_block(inp)
wavfile.write("out.wav", sr, y)
One more note on scaling and the WET
parameter. To maintain overall loudness when adjusting this parameter it's typically better to pan in energy and not in amplitude, .i.e. something like
y[i:i + BLOCK_SIZE] = np.sqrt((1 - WET**2)) * inp + (WET**2) * process_block(inp)
You will also find that the energy in the delayed portion is dependent on the feedback. The higher the feedback, the louder the delay will get. You can compensate this by scaling the delayed part with $\frac{1}{1-g^2}$.