dimhold.by
← Writing

The grid hum dated 111 of 128 recordings to within 5 seconds. One reference file holds the same hour 7 times

For a long time I had a note in my list of ideas: the hum of the mains in a recording can tell when it was made. The grid frequency goes a little up when generation is above the load and a little down when it is below. The same drift reaches every socket of the grid at once. A microphone near the wiring or near a humming device records it, so the hum works as a clock. There are forensic papers about this from 2005. I took it as solved and never checked whether a phone really hears the hum or how long a recording has to be.

NESO publishes the British grid frequency for every second, so first I downloaded August 2026. The lowest value was 49.747 Hz at 17:30 on 31 August by the file’s clock, the highest 50.242 Hz at 22:01 the same day. Only 40% of the seconds were within 50 mHz of the nominal.

The data

The recordings are ENF-WHU from Guang Hua and coauthors, under the MIT licence. 130 recordings around Wuhan University from a smartphone and a voice recorder, in classrooms, offices, dormitories and on campus paths. By my count they are 4.5 to 16 minutes long, 8 kHz mono. For every recording there is the grid signal of its whole day, taken from the mains at 400 Hz. That is 28 day files. The set also has 10 recordings without any hum, which I kept as a control. China runs at 50 Hz, like Europe. The NESO month, rounded to 1 mHz, was my second dataset, a search space 31 times bigger than a day.

I wrote the simplest method I could. Python 3.12, numpy 2.5.3, scipy 1.18.1. A 16 second window moves in 1 second steps. In each window I find the spectral peak near the hum and refine it with a parabola, which gives one frequency per second. This curve slides along the day and the place with the highest Pearson correlation wins. The true place comes from the clean reference of the same recording, as the dataset README suggests. A hit is within 5 seconds of it.

The 50 Hz is almost not there

My first guess was 50 Hz itself. There the search found the right place for 4 recordings of 130.

The hum is there, but higher. At 100 Hz, the second harmonic, the same search found 116. Maybe a phone microphone is weak at 50 Hz because speech does not need it, but I did not check the devices. Choosing 100 Hz after seeing the result is cheating. So the program picks the harmonic from 50 to 350 Hz whose peak stands highest above its neighbourhood. Mostly it picked 100 Hz, but 41 times it took 200 Hz and 6 times 300 Hz.

frequency, Hz, recording 60 against the grid reference 49.96 49.97 49.98 49.99 50.00 50.01 50.02 50.03 0 1 2 3 4 5 6 7 8 9 min grid reference from the recording, 100 Hz divided by 2
The recording is picked by a rule in figdata.py: a hit at 100 Hz, at least 580 seconds of curve, with the error closest to the median of hits. Correlation 0.991, error 3.4 mHz.

With that rule 113 of 130 recordings landed within 5 seconds of the truth. The automatic choice lost 3 against the fixed 100 Hz, all on 200 Hz, where only 29 of 41 were found. On the hits the curve was off by 3.4 mHz in the median, while the grid itself moved by about 19 mHz inside a typical recording.

The miss that was not mine

In an early version of the script recording 91 was the strangest miss. Correlation 0.991, a perfect match by every number I had, but the answer was 2 hours away from the truth. I looked for a bug in my search first. Only after that I compared the day reference with itself.

In 089-107_ref.wav the hour from 1:00 to 2:00 of the file occurs 6 times in a row, up to 7:00. The same samples, every copy 4 samples later than the previous one, which is 10 ms at 400 Hz. At first I saw it as 2 hours repeated 3 times, because a shift of 2 hours matches too. A second check found the period of 1 hour. Later I scanned the file for repeated pieces at any shift. The same hour came out once more, from 9:00 to 10:00, after 2 hours of normal data. The README says the day files are 24 hours long. This one is 23 and only about 17 of them are its own. I cannot say how this happened. The other 27 day files have no repeated piece at all.

day reference 089-107, hours from the start of the file the same hour ×7 0 2 4 6 8 10 12 14 16 18 20 22 recording 91 fits everywhere, correlation 0.9906…0.9907
Every copy matches the hour from 1:00 sample for sample. In the row each copy is 10 ms later than the one before it. Recording 90 falls into the same hours.

For recordings from those hours the reference has 7 almost equally good places. The true place is found by the same search, so it is one of the same 7 copies. In the final run 91 landed on the same copy as its clean reference. 90 is from those hours too and landed 4 seconds from it. Both count as hits, but only because the recording and the clean reference both prefer the earliest copy by the fifth decimal. Neither can be checked, so I count 111 of 128.

2 misses were 7 and 8 seconds off, almost hits. The other 15 are an hour or more off and 12 of them came from recordings where the program chose 200 Hz. Recording 85 matched with a correlation of 0.877 and was 6 hours off. The 10 recordings without hum gave at most 0.506 against any of the 28 days, so 0.877 looks convincing.

What gives it away is the gap to the second best place more than a minute away. For those 15 it was at most 0.035, for hits the median was 0.18. A rule “trust the answer only if the gap is above 0.04” keeps 103 of the 113 hits and refuses 90 and 91. It lets through 1 miss, the one 8 seconds off. But I chose 0.04 by looking at these same 130 recordings and I have no second set to test it on.

What length is enough

Next I cut every recording to its first N seconds and searched again. The harmonic was still chosen on the whole recording. 30 seconds found almost nothing, 16 of 130. At 2 minutes it was 89, which starts to be usable. 5 minutes found 103 of the 129 recordings long enough for it. The window eats 15 seconds, so only 49 recordings give a full 10 minutes of curve. 42 of them were found.

Then the month. This time the bug was in my code. The first run said that a clean 30 second piece of the grid finds its own place only 53 times of 200. At that point I saw correlations above 1, up to 2.67. Sums of values near 50 over 2.7 million seconds lose precision and quiet windows got a variance of almost 0. After I centred the reference before summing, all 200 clean pieces found their place.

For the real test I added to random windows pieces of real extraction error, the difference between a WHU recording and its reference. Here a hit is stricter, within 2 seconds. With the errors 10 minutes found 160 of 200 and 1 minute about half. I also took errors only from the recordings that were hits. Then 10 minutes found 183.

share of pieces placed within the tolerance 0% 25% 50% 75% 100% 30 60 120 300 600 seconds of recording WHU recordings, search in their day GB grid, search in the month, no error GB month plus real errors of all 130 GB month plus errors of the 113 hits
Tolerance is 5 seconds for WHU and 2 seconds for the synthetic month search. Only 49 WHU recordings give 600 seconds of curve. Each point of the month search is 200 random windows.

At 5 and 10 minutes the month came out about 6 points below the day, within the noise of 200 windows. At 30 seconds the month was ahead, 80 of 200 against 16 of 130. So the bigger search space did not visibly cost anything. My guess is that the cleanness of the hum decides more, but these are 2 different grids glued together. Inside 10 minutes the British frequency moves more, a median spread of 26 mHz against 19. How much this helps I did not measure.

What I did not check

I have no recording of my own with a known time. The recordings are from one campus, with devices I know only from the description. The dataset authors have stronger methods with harmonic enhancement that would likely reduce the 15 misses. I did not try video, where the hum comes through the flicker of lights. I also did not check cut and glued recordings, which the same method is used for.