Random numbers from the sound card.
In preparing the material, I rewrote my old C code in 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, data obtained from two Realtek and Audigy 2 sound cards is compared , the results of statistical random tests are presented.
UPD Fixed missing zeros in the code that the UFO ate.
Boring Theoretical Introduction
Almost all programming languages provide several functions for generating the so-called Pseudorandom Numbers . PSHs 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 tasks, a high-quality PSC generator can completely replace truly random numbers. Computer games, process modeling, Monte Carlo integration, genetic algorithms ... the list of tasks for which a good PSH is sufficient can be continued for a long time.
On the other hand, there is a limited circle of problems for which the true randomness of the obtained sequence is important. The main example is cryptography. It is in the context of encryption that people have always been interested in obtaining truly random numbers . The simplest example is the only cipher for which absolute cryptographic strength has been proved, the Vernam cipher (English One-time pad ) requires a sequence of truly random numbers equal in length to the secret message for the key. To ensure cryptographic strength, random data used to generate keys (be it Vernam cipher, AES, RSA or something else) should never be reused. Which leads us to the question of finding a reliable source of random numbers.
A sound card, unlike most other components of a computer, has not only a digital part, but also an analog one.
Consider the (primitive) process of digitizing an audio signal at the linear input of a sound card:
- Initially, we have an electrical signal from some source that carries information about the sound
- The signal enters the analog part of the sound card, where it is amplified to match the dynamic range of the ADC (analog-to-digital converter).
- The signal is digitized by 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:
- radio interference and interference from neighboring devices and radio (have you ever heard how a poorly shielded amplifier catches a radio?)
- power supply interference (in principle, it can also be attributed to radio interference)
- thermal noise of random electron motion in circuit components
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: how much does this noise penetrate the digital part of the sound card? Experience shows that yes - it penetrates, it can be digitized and saved.
In the 16-bit recording mode, only the least significant bit from each sample carries random information; in the 24-bit mode, several least significant ones, but it is most reliable to always take only one least significant bit. How to do this further and we will discuss the example of a Python program for Windows.
Who is not interested in Python: an 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 ordinary DLLs .
We import the sys libraries (for access to standard output) and time (for access to the sleep function ). We also import all the 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 throw a Python exception and the console will already see exactly where the problem is.
Then there is a loop, which for each function of the winmm.dll library (Python object windll.winmm is imported from ctypes ) from the list, we create a variable in the current vars () context , this will allow you to later access 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 " ] :
[ funcname ] = windll. winmm [ funcname ]
vars ( ) [ funcname ] . restype = MMSYSERR_NOERROR
We define the necessary C-structures for working with Windows Audio. The structure class must inherit from the Structure class imported from ctypes , and must contain the _fields_ field listing 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 WAVEFORMATEX structure contains information about the audio data format. 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 type WAVEFORMATEX and allocates a buffer that can store 1 second of audio of this format. An array of C-characters is created by the create_string_buffer function .
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
( "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 the waveFormat object. And three buffers for audio data.
Unfortunately, with most drivers, winmm.dll ( maybe it's a rather old interface) does not allow digitizing more accurately 16 bits even if the audio card supports it. I only know 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 this).
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.
Since this is a C-callback, you must first create a class. This is done by the WINFUNCTYPE function of ctypes , arguments: return type, argument types. Since 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 can be anything, for example, a class indicating where to write data or how much data is needed, etc. POINTER (WAVEHDR) is a pointer to a data buffer. UMsg
parameter indicates the reason for the call, we are interested in MM_WIM_DATA - audio data is available.
= WINFUNCTYPE WRITECALLBACK ( 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 bits: replace at the end of 2 with 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 ) , eight) ]
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 ) ]
Since the data is packed in 2 bytes (3 bytes in the case of 24 bits), we go through the array with a step of 2: range (0, wavBuf.dwBytesRecorded, 2) and select the least significant bit of the least significant byte in the pair: ord (wavBuf.lpData [i]) & 1 . The result is a list of bits of the least significant bits of each sample.
A small mathematical digression:
Random data may have a different distribution, i.e. the frequency of occurrence of a particular number. For example, if we have a sequence of bits 0000100000000, and the position of a unit changes randomly, this is also a random sequence, but it is obvious that the probability of a zero occurring is much higher than units. The most convenient so-called "Uniform distribution" in which the probability of occurrence of zero and one is equal. The procedure for reducing to a uniform distribution is called unbiasing. The simplest method is the replacement: 10 -> 1, 01 -> 0, 11 -> discard, 00 -> discard.
Unbiasing performs the following line
bias = [ bits [ i ] for i in range ( 0 , len ( bits ) , 2 ) if bits [ i ] ! = bits [ i + 1 ] ]
We go through step 2: range (0, len (bits), 2) , throwing the same pairs: bits [i]! = Bits [i + 1] , from the remaining we take the first: bits [i].
Finally, this expression collects our bits into 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 go through step 8, for every 8 bits the reduce function is called (lambda v, b: v << 1 | b, bias [i-8: i], 0) . Here we used a functional programming element, an anonymous function (let's call it temporarily F) lambda v, b: v << 1 | b , called by the reduce function like this: F (... F (F (0, bias [i-8]), bias [i-7]), ..., bias [i-1]) - it turns out a byte that is converted to a character by the chr function .
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 when binary output to the console, Windows does not catch Ctrl + C well.
At the end of the function is a check to see if the buffer is full. If so, then we return it back to the system to fill with the waveInAddBuffer function. If not, this means that the data output is stopped (the device is closed), we decrease the counter of occupied buffers by one (the counter is stored in user data).
We create an instance of the C function of the WRITECALLBACK class from our pythonWriteCallBack function.
Next, after 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 number of ExitFlag).
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 pass to our callback in the form of dwInstance for each event (for example, filling the buffer).
Then we call waveInPrepareHeader for each buffer created and pass them winmm.dll using waveInAddBuffer. Finally, a call to waveInStart (HandleWaveIn) instructs you to start inputting audio. The end we are waiting in the cycle time.sleep (1) .
Exit the program by intercepting the key combination Ctrl + C (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 conversion of the end of a line, the script should be called with the " -u " parameter :
c:\...\python.exe -u Скрипт.py > файл.rndfindings
You can record without an external signal, just “silence”, the low-order bits still get random values. Recording without an external signal (source), we obtain the characteristics of the noise naturally present in the circuits of the audio card. Another option may be to record some signal, then the noise will appear in the digitization errors of the least significant bits.
There are always digitization errors, on the other hand, “zero” noise is highly dependent on the physical device of the audio card and may not appear on some models. The results of the analysis of “zero” noise are discussed below.
The optimal mixer settings for recording obtained experimentally: select the recording channel (not playback!) Line-In , the channel volume to maximum, turn off all other channels.
Microphone channel showed the worst quality of random numbers, due to the fact that many audio cards are trying to "improve" the signal from the microphone and apply various digital filters. On one of the tested audio cards (namely, Realtek ), the microphone channel produced an output unsuitable for receiving 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 received random numbers with the help of several programs. The easiest to use ent (download from http://www.fourmilab.ch/random/random.zip ). Runs from the console.
> type data.rnd | ent.exeWhere data.rndbinary file with random data (do not forget to comment out excess prints in the program, they can spoil the statistics a little). The optimal file size for the test is approx. 500KB The program counts several parameters:
- Entropy (for random data: tends to 8 - the number of bits in a byte)
- Arithmetic mean (for random data: tends to 127.5)
- Monte Carlo Pi number (for 500kB data, the error is about 0.1%)
- Chi-Square (ideally lies in the range of 10-90%)
- Coefficient (for random data close to 0)
It should be remembered that the criteria of randomness are statistical in nature and do not apply 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. 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, because with a certain probability your sound card can generate this article (in fact, this is how I got it - just kidding).
Also, I tested the randomness of the “zero” output of both sound cards using a more sophisticated software package from NIST (US National Institute of Standards). Both cards showed good quality random numbers.
Interestingly, the built-in audio fromRealtek , gives a slightly better distribution uniformity, apparently due to the lower quality of the ADC and high noise. Audigy 2 would be out of competition in 24-bit mode, which is not supported by Realtek (in any case, it needs DirectX, and DirectX in Python is a different 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
Example output of the ent program on a file received by a silence record 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).