📜 ⬆️ ⬇️

Random numbers from sound card

Many have ever been interested in random numbers. I want to share my experiments on obtaining truly random numbers using the "hardware generator" built into almost any computer - a sound card .

When preparing the material, I rewrote my old C code on Python, so this opus is also an example of using the Windows DLL from Python using the standard ctypes library.

At the end of the article the data obtained from the two sound cards Realtek and Audigy 2 are compared, the results of statistical tests for randomness are presented.
')
UPD Fixed the missing zeros in the code that a UFO ate.

Boring theoretical introduction


Virtually all programming languages ​​provide several functions for generating so-called Pseudo Random Numbers . PSCS are not random in the mathematical sense of the word, they are obtained by some well-known algorithm and, knowing the initial parameters of the algorithm, the resulting sequence can always be repeated again. For many problems, a high-quality clock generator can completely replace true random numbers. Computer games, process modeling, Monte-Carlo integration, genetic algorithms ... a list of tasks for which a sufficiently good frequency response can go on and on.

On the other hand, there is a limited range of problems for which the true randomness of the resulting sequence is important. The main example is cryptography. It is in the context of encryption that people have always been interested in the issue of obtaining truly random numbers . The simplest example is the only cipher for which absolute cryptographic robustness is proved, the Vernama cipher (born One-time pad ) requires a sequence of true random numbers for a key equal in length to a secret message. To ensure cryptographic strength, random data used to generate keys (whether it is the Varnam cipher, AES, RSA, or something else) should never be reused. Which leads us to the question of finding a reliable source of random numbers.

The sound card, unlike most other components of the computer, has in itself not only a digital part, but also an analog one.

Consider (primitively) the process of digitizing a sound signal at the line input of a sound card:
  1. Initially, we have an electrical signal from some source that carries sound information.
  2. The signal enters the analog part of the sound card, where it is amplified to correspond to the dynamic range of the ADC (analog-to-digital converter).
  3. The signal is digitized on the ADC with a certain resolution and sampling frequency and enters the digital part of the audio card from where it can already be obtained programmatically.

We are interested in paragraph 2 . As you know, any analog electrical signal inevitably contains noise, the noise components can be roughly divided into several categories:

If the randomness of interference and power is a moot point, then the third type of noise is purely quantum and truly random .

Actually the question is: how much does this noise penetrate the digital part of the sound card? Experience shows that yes - penetrates, it can be digitized and saved.

In the 16-bit recording mode, only the low-order bit from each sample carries random information, while in the 24-bit mode it carries several low-order bits, but the safest thing is to always take only one low-order bit. How to do this further and will be discussed on the example of Python programs for Windows.

Who is not interested in Python: analysis of the results and conclusions at the end, after the description of the program.


Sound recording in Windows


The easiest way to record sound in Windows is to use the Waveform Audio interfaces from the winmm.dll library. There is no standard library for working with sound in Python, so we will use the ctypes library, which provides an interface to regular DLLs .

We import the sys libraries (to access the standard output) and time (to access the sleep function). Also import all names from ctypes .

import sys
import time
from ctypes import *


For C functions that return an integer (usually an error code), Python allows you to specify a function that will check the error code. We will use this to add minimal error control in the program: if the C-function returns an error, MMSYSERR_NOERROR will cause a Python exception and in the console you will see exactly where the problem is.

Then there is a loop that for each function of the winmm.dll library (the Python object windll.winmm is imported from ctypes ) from the list, we create a variable in the current context vars () , this will allow later access to the function simply by name (waveInOpen instead of windll.winmm .waveInOpen). We also assign the return type to our “controlling” function MMSYSERR_NOERROR.

def MMSYSERR_NOERROR ( value ) :
if value ! = 0 :
raise Exception ( "Error while running winmm.dll function" , value )
return value

for funcname in [ "waveInOpen" , "waveInPrepareHeader" ,
"waveInAddBuffer" , "waveInStart" ,
"waveInStop" , "waveInReset" ,
"waveInUnprepareHeader" , "waveInClose" ] :
vars ( ) [ funcname ] = windll. winmm [ funcname ]
vars ( ) [ funcname ] . restype = MMSYSERR_NOERROR


We define necessary for working with Windows Audio C-structure. The structure class must inherit from the Structure class imported from ctypes , and must contain the _fields_ field, which lists the names and C-types of structure elements. We imported classes for C-types from ctypes , their names speak for themselves: c_int, c_uint, etc.

The first structure WAVEFORMATEX contains information about the format of the sound data. Without parameters, the constructor will create a structure with typical values ​​for most sound cards: 16bit 48kHz mono .

The second WAVEHDR describes a buffer for audio data. As a parameter, the constructor requires an object of the WAVEFORMATEX type and allocates a buffer capable of storing 1 second of audio of this format. The array of C-characters is created by the function create_string_buffer .

class WAVEFORMATEX ( Structure ) :
WAVE_FORMAT_PCM = 1
_fields_ = [ ( "wFormatTag" , c_ushort ) ,
( "nChannels" , c_ushort ) ,
( "nSamplesPerSec" , c_uint ) ,
( "nAvgBytesPerSec" , c_uint ) ,
( "nBlockAlign" , c_ushort ) ,
( "wBitsPerSample" , c_ushort ) ,
( "cbSize" , c_ushort ) ]

def __init__ ( self , samples = 48000 , bits = 16 , channels = 1 ) :
self . wFormatTag = WAVEFORMATEX. WAVE_FORMAT_PCM
self . nSamplesPerSec = samples
self . wBitsPerSample = bits
self . nChannels = channels
self . nBlockAlign = self . nChannels * self . wBitsPerSample / 8
self . nAvgBytesPerSec = self . nBlockAlign * self . nSamplesPerSec
self . cbSize = 0

class WAVEHDR ( Structure ) :
_fields_ = [ ( "lpData" , POINTER ( c_char ) ) ,
( "dwBufferLength" , c_uint ) ,
( "dwBytesRecorded" , c_uint ) ,
( "dwUser" , c_uint ) , # User data dword or pointer
( "dwFlags" , c_uint ) ,
( "dwLoops" , c_uint ) ,
( "lpNext" , c_uint ) , # pointer
( "reserved" , c_uint ) ] # pointer reserved
def __init__ ( self , waveformat ) :
self . dwBufferLength = waveformat. nAvgBytesPerSec
self . lpData = create_string_buffer ( ' \ 0 00' * self . dwBufferLength )
self . dwFlags = 0


Next, we create a waveFormat object. And three buffers for audio data.

Unfortunately, with the majority of winmm.dll drivers (because this is a fairly old interface), it doesn’t allow digitizing more precisely 16 bits even if the audio card supports it. I know only one card: SB Live24bit, which it could. Now it is not at hand, but there is an Audigy 2 Notebook, which writes 24 bits only in DirectX or ASIO. Therefore, today's example is designed for 16 bits (the place that needs to be changed for 24 bits is marked with a comment, in case your card supports it).

waveFormat = WAVEFORMATEX ( samples = 48000 , bits = 16 )
waveBufferArray = [ WAVEHDR ( waveFormat ) for i in range ( 3 ) ]


The next function is the main one in the program; it will be called by Windows when winmm.dll fills one of our buffers.

Tk is a C-callback, you first need to create a class. This is done by the WINFUNCTYPE function of ctypes , arguments: return type, argument types. Tk, our function should not return anything, the first argument is None , the rest according to MSDN. Pay attention to the argument POINTER (c_uint) - this is a pointer to user data, in the example it is just a number, but there could be anything, such as a class indicating where to write data or how much data is needed and so on. POINTER (WAVEHDR) is a pointer to a data buffer.

Parameter uMsg indicates the reason for the call, we are interested in MM_WIM_DATA - audio data is available.

WRITECALLBACK = WINFUNCTYPE ( None , c_uint, c_uint, POINTER ( c_uint ) , POINTER ( WAVEHDR ) , c_uint )
def pythonWriteCallBack ( HandleWaveIn, uMsg, dwInstance, dwParam1, dwParam2 ) :
MM_WIM_CLOSE = 0x3BF
MM_WIM_DATA = 0x3C0
MM_WIM_OPEN = 0x3BE
if uMsg == MM_WIM_OPEN:
print "Open handle =" , HandleWaveIn
elif uMsg == MM_WIM_CLOSE:
print "Close handle =" , HandleWaveIn
elif uMsg == MM_WIM_DATA:
#print "Data handle =", HandleWaveIn
wavBuf = dwParam1. contents
if wavBuf. dwBytesRecorded > 0 :
bits = [ ord ( wavBuf. lpData [ i ] ) & 1 for i in range ( 0 , wavBuf. dwBytesRecorded , 2 ) ]
# for 24 bit: replace at the end of 2 by 3 in the previous line
bias = [ bits [ i ] for i in range ( 0 , len ( bits ) , 2 ) if bits [ i ] ! = bits [ i + 1 ] ]
bytes = [ chr ( reduce ( lambda v, b: v << 1 | b, bias [ i- 8 : i ] , 0 ) ) for i in range ( 8 , len ( bias ) , 8 ) ]
rndstr = '' . join ( bytes )
#print bytes,
sys . stdout . write ( rndstr )
if wavBuf. dwBytesRecorded == wavBuf. dwBufferLength :
waveInAddBuffer ( HandleWaveIn, dwParam1, sizeof ( waveBuf ) )
else :
print "Releasing one buffer from" , dwInstance [ 0 ]
dwInstance [ 0 ] - = 1
else :
raise "Unknown message"


Key points:
bits = [ ord ( wavBuf. lpData [ i ] ) & 1 for i in range ( 0 , wavBuf. dwBytesRecorded , 2 ) ]

Tk data is packed by 2 bytes (3 bytes in the case of 24 bits) we go through the array in step 2: range (0, wavBuf.dwBytesRecorded, 2) and select the low-order bit of the lower byte in the pair: ord (wavBuf.lpData [i]) & 1 . The result will be a list of bits of the lower bits of each sample.

Minor Methematic Retreat:

Random data may have a different distribution, i.e. frequency of loss of a particular number. For example, if we have the sequence of bits 0000100000000, and the position of the unit changes randomly, this is also a random sequence, but it is obvious that the probability of a zero being dropped is much higher than one. The most convenient so-called. "Uniform distribution" in which the probability of occurrence of zero and one is equal. The procedure for bringing to a uniform distribution is called unbiasing. The simplest method is the replacement: 10 -> 1, 01 -> 0, 11 -> discard, 00 -> discard.

Unbiasing performs the next line.
bias = [ bits [ i ] for i in range ( 0 , len ( bits ) , 2 ) if bits [ i ] ! = bits [ i + 1 ] ]

We pass in step 2: range (0, len (bits), 2) , throwing out identical pairs: bits [i]! = Bits [i + 1] , from the remaining take the first: bits [i].

Finally, this expression collects our bits in bytes and merges everything into a string.
bytes = [ chr ( reduce ( lambda v, b: v << 1 | b, bias [ i- 8 : i ] , 0 ) ) for i in range ( 8 , len ( bias ) , 8 ) ]

We pass with step 8, for each 8 bits, the function reduce is called (lambda v, b: v << 1 | b, bias [i-8: i], 0) . Here an element of functional programming is used, an anonymous function (let's call it temporarily F) lambda v, b: v << 1 | b , called by the function reduce: F (... F (F (0, bias [i-8]), bias [i-7]), ..., bias [i-1]) - it turns out a byte, which is converted to a character by the function chr .

The list of bytes is converted to a string that is written to stdout , since this binary data is better to redirect the output to a file. If you want to write to the console, then it is better to do this through " print bytes, ", because with binary output to the console, Wind doesn’t catch Ctrl + C.

At the end of the function it is worth checking whether the buffer has been completely filled. If yes, then we return it back to the system for filling with the waveInAddBuffer function. If not, this means that the data output is stopped (the device is closed), we reduce the counter of occupied buffers by one (the counter is stored in user data).

Create an instance of the C function of the WRITECALLBACK class from our pythonWriteCallBack function.

Next, defining several useful constants, open the WAVE_MAPPER device (you can directly set the sound card number, they are numbered starting from zero: 0, 1, 2) first with the WAVE_FORMAT_QUERY parameter to check that the format is supported, then with the CALLBACK_FUNCTION parameter, specifying our function and user data (in our case, the ExitFlag number).

Notice how pointers are passed from Python to the C function using the byref function. We also passed a pointer to our “user data” in byref (ExitFlag) , which Windows will send to our callback as dwInstance at every event (eg, buffer filling).

Then we call the waveInPrepareHeader for each created buffer and pass them to winmm.dll using waveInAddBuffer. Finally, the waveInStart (HandleWaveIn) call gives the command to start audio input. We are waiting for the end in the time.sleep (1) loop.

Exit the program by intercepting the Ctrl + C key combination (Python KeyboardInterrupt ).
writeCallBack = WRITECALLBACK ( pythonWriteCallBack )
try :
ExitFlag = c_uint ( 3 )
HandleWaveIn = c_uint ( 0 )
WAVE_MAPPER = c_int ( - 1 )
WAVE_FORMAT_QUERY = c_int ( 1 )
CALLBACK_FUNCTION = c_int ( 0x30000 )

waveInOpen ( 0 , WAVE_MAPPER, byref ( waveFormat ) , 0 , 0 , WAVE_FORMAT_QUERY )
waveInOpen ( byref ( HandleWaveIn ) , WAVE_MAPPER, byref ( waveFormat ) , writeCallBack, byref ( ExitFlag ) , CALLBACK_FUNCTION )

for waveBuf in waveBufferArray:
waveInPrepareHeader ( HandleWaveIn, byref ( waveBuf ) , sizeof ( waveBuf ) )
waveInAddBuffer ( HandleWaveIn, byref ( waveBuf ) , sizeof ( waveBuf ) )

waveInStart ( HandleWaveIn )

while 1 :
time . sleep ( 1 )

except KeyboardInterrupt :
waveInReset ( HandleWaveIn )

while 1 :
time . sleep ( 1 )
if ExitFlag. value == 0 :
break

for waveBuf in waveBufferArray:
waveInUnprepareHeader ( HandleWaveIn, byref ( waveBuf ) , sizeof ( waveBuf ) )

waveInClose ( HandleWaveIn )


To disable automatic end-of-line conversion, the script should be called with the " -u " parameter:
c:\...\python.exe -u .py > .rnd

findings



You can record without an external signal, just "silence", the low bits still get random values. Recording without an external signal (source), we get the characteristics of the noise naturally present in the circuits of the sound card. Another option would be to record some signal, then the noise will be manifested in the digitization errors of the low-order bits.

Digitization errors are always there, on the other hand “zero” noise strongly depends on the physical device of the audio card and may not appear on some models. Further, the results of the analysis of "zero" noise are discussed.

The optimal mixer settings for recording, obtained experimentally: choose a recording channel (not playback!) Line-In , loudness of the channel to the maximum, all other channels are turned off.

The Microphone channel showed the worst quality of random numbers, which is explained by the fact that many audio cards try to “improve” the signal from the microphone and use various digital filters. On one of the tested audio cards (namely, Realtek ), the microphone channel produced an unusable output for the midrange. On Audigy 2, turning on the Mic Boost + 20DBA microphone gain also removed the random component.

You can test the quality of the random numbers obtained using several programs. Simplest use of ent (download from http://www.fourmilab.ch/random/random.zip ). Runs from console
> type data.rnd | ent.exe
Where data.rnd is a binary file with random data (do not forget to comment out extra print'y in the program, they can spoil the statistics a bit). The optimal file size for the test is approx. 500KB. The program calculates several parameters:


It should be remembered that randomness criteria are statistical in nature and are not applied to the sequence itself, but to the source. Therefore, to obtain reliable results, it is necessary to conduct a series of tests on samples of one source. For example, 20 pieces of 500KB each. If the majority passed the test (approx. 90%), then the source is random with a certain probability.

Unfortunately, there are no 100% criteria in statistics, since with a certain probability, your sound card can generate this article (actually, this is how I got it - just kidding).

Also, I tested the randomness of the “zero” output of both sound cards with the help of a more sophisticated software package from NIST (US National Standards Institute). Both cards showed good quality random numbers.

Interestingly, the built-in audio from Realtek , gives a slightly better uniformity of distribution, apparently due to the lower quality of the ADC and high noise. Audigy 2 would be out of competition in 24-bit mode, which Realtek is not supported (in any case, it needs DirectX, and DirectX in Python is a separate story).

Links to other test packages:
http://stat.fsu.edu/pub/diehard/
http://www.phy.duke.edu/~rgb/General/dieharder.php
http://csrc.nist.gov/groups/ST/toolkit/rng/documentation_software.html

An example of the output of the program ent on a file obtained by recording silence from the Audigy 2 line input:
Entropy = 7.999609 bits per byte.

Optimum compression would reduce the size
of this 500000 byte file by 0 percent.

Chi square distribution for 500000 samples is 270.78, and randomly
would exceed this value 23.75 percent of the times.

Arithmetic mean value of data bytes is 127.5890 (127.5 = random).
Monte Carlo value for Pi is 3.139644559 (error 0.06 percent).
Serial correlation coefficient is 0.001109 (totally uncorrelated = 0.0).

Source: https://habr.com/ru/post/62237/


All Articles