Bitnoise
Introduction
bitnoise
is a 1-bit noise generator, based on the
one found on the NES APU.
It features a 15-bit linear feedback shift register
(abbreviated as LFSR
), mode toggle switch, and a
rate parameter for speed control.
Tangled Files
Bitnoise tangles to two files: bitnoise.c
and
bitnoise.h
.
/* tangled from sndkit. do not edit by hand */
#include <stdint.h>
#include <math.h>
#define SK_BITNOISE_PRIV
#include "bitnoise.h"
<<macros>>
<<funcs>>
If SK_BITNOISE_PRIV
is enabled, it
exposes the struct in the header. Otherwise, it is
left as an opaque struct.
/* tangled from sndkit. do not edit by hand */
#ifndef SK_BITNOISE_H
#define SK_BITNOISE_H
#ifndef SKFLT
#define SKFLT float
#endif
<<typedefs>>
<<funcdefs>>
#ifdef SK_BITNOISE_PRIV
<<structs>>
#endif
#endif
Structs
All parameters are contained in a struct called
sk_bitnoise
.
typedef struct sk_bitnoise sk_bitnoise;
struct sk_bitnoise {
<<sk_bitnoise>>
};
Init
Bitnoise is initialized with sk_bitnoise_init
. The
sampling rate must be supplied here.
void sk_bitnoise_init(sk_bitnoise *bn, int sr);
void sk_bitnoise_init(sk_bitnoise *bn, int sr)
{
<<init>>
}
Variables, Constants, and State
Phasor Constants and State
A fixed-point phasor is used to manage clocking and frequency control, similar to the one found in rline.
Defined constants SK_BITNOISE_PHSMAX
and
SK_BITNOISE_PHSMSK
are used to wrap.
#define SK_BITNOISE_PHSMAX 0x1000000L
#define SK_BITNOISE_PHSMSK 0x0FFFFFFL
To calculate the increment amount, a calculated constant
called maxlens
is used, which is the maximum phase length
in units of seconds. When multiplied by the rate paramter,
this provides a value (in units of cycles) that tells the
phasor how much to increment.
SKFLT maxlens;
bn->maxlens = SK_BITNOISE_PHSMAX / (SKFLT) sr;
The phasor position itself is stored in a long integer
called phs
, and is set to be 0.
uint32_t phs;
bn->phs = 0;
Linear Feedback Shift Register State
Stored in a 16-bit unsigned integer called lfsr
.
uint16_t lfsr;
According to the APU specs, this is initialized to be 1.
bn->lfsr = 1;
Bit Position
The current bit position in the register is kept track of in
an integer called pos
.
int pos;
bn->pos = 0;
Saved Value
The last computed sample is stored in a variable called
saved
.
SKFLT saved;
bn->saved = 0;
Parameters
Rate
The rate
parameter changes the speed at which the noise
generator updates, similar to how a sample-and-hold works.
This is supplied in units of Hz.
Set the rate parameter with sk_bitnoise_rate
.
void sk_bitnoise_rate(sk_bitnoise *bn, SKFLT rate);
void sk_bitnoise_rate(sk_bitnoise *bn, SKFLT rate)
{
bn->rate = rate;
}
SKFLT rate;
sk_bitnoise_rate(bn, 1000);
Mode
The mode
parameter is a toggle value which changes the
behavior of LFSR. It is either 1 or 0. When 0, the LFSR
should be set up to produce a sequence that is 32767 steps
long. When 1, it should produce 31 or 91 steps, depending on
the state of the shift register.
Set the mode parameter with sk_bitnoise_mode
.
void sk_bitnoise_mode(sk_bitnoise *bn, int mode);
void sk_bitnoise_mode(sk_bitnoise *bn, int mode)
{
bn->m = mode;
}
int m;
sk_bitnoise_mode(bn, 0);
Compute
A single sample is initialized with sk_bitnoise_tick
.
SKFLT sk_bitnoise_tick(sk_bitnoise *bn);
SKFLT sk_bitnoise_tick(sk_bitnoise *bn)
{
SKFLT out;
out = 0;
<<update_phasor>>
<<update_LFSR>>
<<write_output>>
return out;
}
To begin, the fixed phasor is updated. It is incremented by
an amount determined by multiplying the constant maxlens
with the rate
parameter.
bn->phs += floor(bn->rate * bn->maxlens);
When the phasor reaches the or goes beyond the upper limit, it needs to wrap around again. Also, the state of the shift register may need to be updated.
Wrap around of the phasor is done by AND-ing it with the
phase mask SK_BITNOISE_PHSMSK
.
The shift register will need to get updated if it bit position reaches the end (it exceeds 14).
According to the NES dev wiki, the LFSR is computed in the following way:
Compute feedback as the exclusive OR of bit 0 and one other bit. Depending on the mode flag, this bit is either bit 1 (mode OFF) or bit 6 (mode ON).
The register is right-shifted by 1.
The calculated feedback bit is set to be bit 14 (the leftmost bit) of the new register.
x = bn->lfsr;
f = (x & 1) ^ ((x >> (bn->m ? 6:1)) & 1);
x >>= 1;
x |= f << 14;
bn->lfsr = x;
The actual noise output is done by extracting the current bit from the shift register, and then scaling that state to be in range -1,1.
The bitwise operations below work together to "pop" the current bit out of the register. First, the register is right-shifted so that the desired bit is in the lowest bit position. ANDing with 1 then isolates that last bit.
y = (bn->lfsr >> bn->pos) & 1;
The value is scaled to be in between -1 and 1. Because
it is binary, one could be tempted to use a ternary value
like y ? 1.0 : -1.0
. However, according to
[[https://godbold.org][godbolt], y * 2 - 1
takes about 3
instructions to do in x86_64 (mov
, mul
, sub
), and
y ? 1.0 : -1.0
takes instructions (cmp
, je
, mov
,
jmp
, mov
). (I'm pretty sure the one with less
instructions is more efficient).
This computed output is cached for later use in saved
.
bn->saved = y * 2 - 1;
if (bn->phs >= SK_BITNOISE_PHSMAX) {
SKFLT y;
bn->phs &= SK_BITNOISE_PHSMSK;
if (bn->pos > 14) {
uint16_t f;
uint16_t x;
bn->pos = 0;
<<calculate_LFSR>>
}
<<extract_bit>>
<<scale_and_store>>
bn->pos++;
}
The cached value saved
is what is returned in the output.
out = bn->saved;