1

(16 replies, posted in General Discussion)

Utz, it seems like you are well beyond getting CARN to work so won't need this anymore, but as promised here's my 1-bit CARN code for anyone else who might still wonder about the basic idea of the implementation.

Screengrab from gen~:
gen code implementing 1-bit CARN

And just the text copied:

// 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 Td(100);           // "average noise period" (in samples)
History count(0);          // how far, in samples, are we into a window
History timeToImpulse(0);  // time to next impulse, between 0 and 2*(Td-1), called karn(m)-karn(m-1) 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
Td = round(samplerate/density);              // average noise period for the specified pulse densities, eq (3) in the paper

// counter keeps track of time since last impulse
count = count + 1; // increment counter

if (count >= timeToImpulse) { // if it is time for an impulse
    out1 =  1; // make a +1 pulse
    count = 0; // start the counter over
    timeToImpulse = round(((noise()+1.0)/2.0)*2*(Td-1)); // set a delay for the next impulse
} else {       // otherwise
    out1 = 0;  // the output should always be zero
}

2

(16 replies, posted in General Discussion)

Also that low shelf characteristic of COVN got me thinking. What if we were to also apply a lower limit to the random number range? Then the result should become tonal at some point, right?

Yes! For COVN, you can constrain it so that the impulse can only appear in a narrower part of the window. Actually Välimäki et al. study that just a little bit in their article, where they call it "Extended Velvet Noise" (EVN, his equations 6–7), and define a parameter "Δ" (between 0.0 and 1.0) representing the proportion of the window where the impulse may live. So I guess the 1-bit or parameterized version would be CEVN ("Crushed Extended Velvet Noise") ;-D. At the extreme limit where the random number is constrained to lie exactly at the start of the window, you just get a normal, tonal impulse train with a frequency of 1/Td.

There is *also* an extra parameter in CARN which I didn't mention, but is discussed in my paper (see eqn. 6) and also in Välimäki et al.'s article. This extra parameter, again called "Δ", balances how much of the time offset in CARN comes from a fixed offset, and how much comes from the random offset. I've been discussing the case where it is entirely random (Δ=1.0) so far, but the limit as Δ approaches 0.0 is, again, just a normal impulse train!

By setting Δ between 0.0 and 1.0 for either CEVN or CARN, you get a sort of noisy impulse train, which can be very useful. I've experimented using CEVN with a small Δ especially, and it is very useful musically. Take a listen to the attached .wavs, which are CEVN and CARN versions of a "melody" (played with the pulse density) sweeping Δ. Each one plays one bar of the riff at Δ=0.0, the sweeps Δ from 0.0 up to 1.0 over 6 bars, then plays one bar at Δ=1.0.

3

(16 replies, posted in General Discussion)

Thanks a lot for the detailed explanation! It's all very cle... wait, so there's no backtracking involved, eg. the pulse location in window n+1 is independent of the pulse location in window n? If so, what's the difference between COVN and CARN, then?

Yes, you're correct that there's no backtracking. The difference between COVN and CARN is:
    —COVN: Break time into segments of equal length (the "windows"). At the start of each window, select a random sample in that window to set to 1 and let all the others be zero. So yes, for COVN, the pulse location in window m+1 is totally independent of the pulse location in window m.
    —CARN: Each time you set a sample to 1, select a new random number to represent the number of samples to wait before another sample is set to 1, letting all other samples in between equal zero. So, for CARN, there aren't really windows as such, and pulse m+1 does depend on the location of pulse m—the location of pulse m+1 is specified as an offset from pulse m.
I will try to comment up my CARN code and attach it later today to make this more clear.

A question, it sounds like there is a drop in pitch with increasing window size. Is this expected, or is it an effect of the bad PRNG?

Yes! The cutoff frequency for COVN and CARN depends directly on the pulse density or the window size. So the spectrum will include relatively more low frequencies as the pulse density decreases (or the window size increases), and relatively more high frequencies as the pulse density increases (or the window size decreases). That's not exactly as easy to see in the paper at a glance, since you have to compare across multiple figures. For instance if you look at Figure 2, the four sub-figures show the PSD for COVN with pulse densities of Nd = 1000, 2000, 4000, and 8000 pulses/second. The cutoff frequency in each case is approximately Nd/2 Hz. Or, you could say it is fs/Td/2, where fs is the sampling rate and Td is the window length in samples.

I rendered a new figure for you too (below and attached), that shows the PSD of COVN for the 1-bit case only (p=1.0), with each trace being a different pulse density. That also shows more clearly how the pulse density controls the cutoff frequency and the amplitude simultaneously. Maybe it is easy to miss on the original figure that the y-axis scale is changing with the pulse density.

PSD for 1bit COVN at different pulse densities

Your ZX Spectrum tests sound great! I ran them on Fuse and the first one sounds a little "metallic" compared to my COVN. I'm guessing that is the result of the PRNG being less random or looping faster than my Matlab/rand() or MaxMSP/noise() functions. Hell, that's probably not even necessarily a bad thing. It sounds interesting and does still have the same general spectral shape. The second test sounds really fantastic, it seems like you are sweeping the pulse density to get the "filter sweep" / "envelope" sound, right? Very exciting! smile

If you have any more "tricks" to teach, please don't hold back! Generally any sort of signal processing with low computational requirements is of interest (never mind the "1-bit-ness" of things, it's possible to simulate multiple volume levels on 1-bit).

I've developed a few other synthesis techniques to use in my own music, I'll be happy to share some of them when I can figure out a good way to explain them. More on that soon. smile

4

(16 replies, posted in General Discussion)

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. smile

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. smile

--------

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
}

5

(16 replies, posted in General Discussion)

Thank you both for the kind words! And thank you for the pointer to Shiru's work, I will be sure to mention his work next time. I've been marvelling at "System Beep"!

I'll write up a proper response about COVN &c. and some pseudocode to try to start answering your questions as soon as I can.

6

(16 replies, posted in General Discussion)

Dear 1-bit forum,

I wanted to share "Generalizations of velvet noise and their application to 1-bit music," a scientific paper I just wrote and presented at DAFx 2019 (the international conference on digital audio effects). The paper is freely available here:
http://dafx2019.bcu.ac.uk/papers/DAFx2019_paper_53.pdf.
I hope people can take a look through the paper, since I think that it proposes techniques that could be really useful in 1-bit composition!

This paper proposes a family of sparse, ternary (-1, 0, and +1), noise sequences I call "crushed velvet noise." A special case which I look at in some detail is binary (0 and +1 only), making it compatible with 1-bit music sound synthesis and composition. These crushed velvet noise sequences are a perfect fit for 1-bit music for a few reasons:
1) Their sparsity means they can be layered with other 1-bit sounds using the classic XOR trick, the same way that you would use to layer many thin pulse waves or pulse trains.
2) Crushed velvet noise offers a small but useful degree of spectral control, compared to just a random sequence of 0s and 1s. One variety (CTRN: crushed totally random noise) has a flat/white spectrum, and another (COVN: crushed original velvet noise) has a low shelf spectrum whose cutoff depends on the pulse density.
3) Crushed velvet noise offers a level of volume control which is hard to get in general in the 1-bit domain. All three proposed varieties sound louder when the pulse density is higher, and softer when the pulse density is lower. Note however that when the pulse density is too low, they no longer sound like "smooth" continuous noise, but rather like crackles.

I hope that these new noise sequences can be useful to some of you, and would be thrilled if this finding could help add any new features to any of your trackers or a new sound synthesis approach to your bag of tricks. I would be very happy to answer any questions that anyone may have.

Apologies too if this has already been devised by someone. I tried to give an accurate view on 1-bit music in the paper, but I'm sure there is lots that I don't know, and especially that people may use varied sound synthesis algorithms that I am unaware of. I intend to continue this research thread, and would want to give credit to prior work to the best of my ability and knowledge.

All the best,
Kurt

p.s. The following link has a 1-bit cover of "Unholy Captives" from Lucas Pope's "Return of the Obra Dinn"—a game known for its 1-bit *graphics*:
https://soundcloud.com/kurt-james-werne … vertribute
I used my crushed velvet noise approach described in the paper for some of the percussion, and a number of other techniques I have devised as well, some of which are described briefly at the link.