First some clarification on the notation and how it works. It seems that you have mostly figured it out, but I'll keep this just in case. To quickly answer your question: I think you can ignore "m" once you understand the code below. Also I would point out one small mistake—you only need one random number sequence, to decide the location of the impulse. You don't need the second one for 1-bit music, since it's only used to decide the sign of the impulse and that will always be +1 by construction, in the 1-bit context.
At the end I'll post some code that I think will make it a bit more clear too. Please let me know what other questions come up that I can clarify, I'm very excited to help.
--------
The paper is notated assuming that there is a fixed sampling rate, for instance 44,100 Hz for CD audio, or the higher-quality 96,000 Hz (the one I actually used). In that context, the discrete-time sample index is just the integer time variable. For example, a discrete-time 100 Hz sine wave would be described by y[n] = sin(2*pi*n*100/Fs), where Fs is the sampling rate (for instance 44,100) and n is a sequence of integers 0, 1, 2, 3, 4, etc. I'm realizing now that perhaps old-school 1-bit trackers don't even have a fixed sample rate the way that modern audio processing (e.g. in a VST) does, but that in your context you deal just with raw clock cycles of the computer (up in the MHz?). Is that correct?
And if so, does that mean that on the ZX Spectrum you don't think in terms of audio samples in discrete time (e.g. a pulse wave like 000100010001) but rather just the time to flip up to +1 and a time to flip back down to 0, which need to be very close for a pulse train? I probably have a lot to learn about the terminology and basic idea of your approaches, so thanks for bearing with me here.
"Discrete pulse index" and "window width" and are terms related specifically to velvet noise. Window width is just, for OVN and COVN, the size in samples of each "window" or "frame" which includes a single pulse. For instance with fs=44100 and a pulse density Nd=100 (pulses per second), you could get a window width of Td=441 samples. Then the (C)OVN algorithm involves randomly picking one out of each group of 441 samples and setting it to +1, leaving the rest as 0s.
"Discrete pulse index" is just the integer (k in my paper) telling you which pulse you have. So the first pulse that is generated has the index k=0, the second has the index k=1, etc. I notated it in that way mostly to match the notation of the earlier papers on velvet noise, but to be honest, that is probably not the best way to think about implementing things, since there is no real need to keep track of *which* ever-increasing index a pulse has. It probably just makes sense to keep track of timings till the next sample for COVN and CARN, as you suggested. In general, for all of the techniques I proposed, it is probably much better, for implementation purposes, to just understand the *concept* and implement them however seems best, especially if you would be thinking in terms of continuous times.
Here is an example (image of screengrab attached and copy-paste of code as text below) of how to code 1-bit COVN which is written in the "codebox" object of Gen~ inside of Max/MSP. Codebox is a sort of C-like mini language, so I think it should be pretty legible in general. The only weird thing is that variables are all defined as "History" objects at the top of the code. Those are only executed once when the code is loaded, whereas the rest of the code runs every sample. I suppose it's doubly strange because they are defined there rather than in a header file or constructor, and that they are all called "History"! But you can just ignore that in understanding it... suffice to say the lines starting with "History" just defined variables and starting values, and you can ignore them otherwise.
The "noise()" function just returns a random number between -1 and +1, so you would replace that with whatever reasonable random or pseudo-random number generator is appropriate. I think it is a nice open question whether pseudo-random number generators will work, but I am quite hopefully that they would drop in to replace my "noise()" RNG with no problems. This is my impression for two reasons:
1) I would predict (but haven't tested) that any issues with a "looping" sound of XORshift or LFSR noise would be greatly reduced in 1-bit crushed velvet noise. Say you had an XORshift or LFSR that made a 1023-sample long sequence. I think we could all agree that will sound very "looped". However, if it is used for velvet noise with a pulse density of 4000 samples/second, it essentially makes it so that the periodicty is 4,000*1,023=4,092,000 samples long, since only one random number is used per *window* rather than per one per *sample*. I'm not sure if this reasoning is right though, since in the first case you only need one binary number per sample, but in the second case you need a more complex random number (in this case, between 0 and 3,999) per window. But that's my intuition at least, I am very happy to be told I'm wrong since I don't know much about how you would be generating random numbers.
2) Prof. Vesa Välimäki, one of the pioneers of (non-crushed) velvet noise, also defined an approach called "Random Integer Noise" which uses a finite-length table of random numbers to generate ARN, and that seems to work fine. It's described in section IIB of his 2013 paper "A Perceptual Study on Velvet Noise and Its Variants at Different Pulse Densities" which is freely available as an author's preprint here: https://www.researchgate.net/publicatio … _Densities.
I would also mention here that on account of 1) above, you will probably find that CTRN is far, far less interesting than COVN. Since CTRN, no matter how you slice it, requires one random number per sample, and COVN requires only one random number per window, a factor of samplerate/Nd lower. For instance considering a sample rate of 9,6000, that is 12x fewer random numbers needed for Nd=8000 or 96x fewer random numbers needed for Nd=1000.
--------
// the next 4 lines (History) just set up variable definitions, they are NOT run each sample but just once
History density(20); // number of pulses/second, called Nd in the paper
History windowLength(100); // length of window (in samples), called Td in the paper
History count(0); // how far, in samples, are we into a window
History impulseLocation(0); // the location, between 0 and windowLength-1, of the pulse in a window, called kovn in the paper
// the actual code that is executed each sample is below..........
density = in1; // set the density based on an input signal
density = min(max(density, 0),samplerate/2); // make sure density isn't less than 0 or greater than half the sampling rate
windowLength = round(samplerate/density); // window length to achieve the specified pulse densities, eq (3) in the paper
// counter keeps track of position within a window
count = count + 1; // increment counter
if (count >= windowLength) { // if the window has ended, by count going above window length
count = 0; // start the counter over
impulseLocation = round(((noise()+1.0)/2.0)*(windowLength-1)); // set a location for the pulse in the next window
}
if (count==impulseLocation) { // if it is time to trigger an impulse
out1 = 1.0; // make a +1 pulse
} else { // otherwise
out1 = 0; // the output should always be zero
}
Post's attachmentsScreen Shot 2019-09-06 at 10.15.45 AM.png 285.32 kb, file has never been downloaded.
You don't have the permssions to download the attachments of this post.