Sound, read from an acoustician's source

A smith raises a hammer over an anvil in a forge, and rings of sound spread across the room, fainter with distance, toward a worker wearing ear defenders.
A forge, with the sound of the anvil spreading to a worker in ear defenders.

This essay is not occupational-health advice. It describes published limits as figures on a page and arithmetic with published formulas, and it reads a program that states them. Whether a worker is protected is decided by a measurement and a hearing conservation programme, and a dose over a limit is not a finding of hearing loss. Nothing here is a diagnosis or a treatment, and nothing in it is a recommendation for anyone's ears. Ask an occupational-health professional.

In 1924 the Bell Telephone Laboratories replaced an earlier unit with the Transmission Unit, defined so that the number of units was ten times the base-10 logarithm of the ratio of a measured power to a reference power. In 1928 the Bell system renamed the unit the decibel, and the bel, from which the decibel takes its name, was named in honor of the telecommunications pioneer Alexander Graham Bell. The unit began at the Bell Telephone Laboratories as a ratio of powers, and the same logarithm now states the pressure of a sound, and the dose a worker's day holds.

The sound library for DKE, the Deterministic Knowledge Engine, writes down some of what an acoustician teaches in DKE Python, and this essay teaches the subject by reading it. It is two halves that share nothing but the subject. The first records three kinds of fact. An exposure is a level in decibels held for some hours, with the limit it is judged against, and the library's rule concludes whether it is over that limit. A worker's day is a set of segments, each at its own level, and the library sums their doses and concludes whether the day is over. A tone is a frequency, and the library concludes whether it lies in the range of hearing, below it or above it. The second half calculates: the hours a limit permits, the level of a pressure, the level of two sources added, the fall of a level with distance, the speed of sound, a wavelength, the Doppler shift and the 8-hour average level of a dose. Every program below runs as shown once the library's source file has been compiled into a store, and every output is copied from a run. The programs name their own subjects and print only those, so that a store which already holds other exposures and tones gives the same lines.

1. A limit is two numbers

The occupational limits for noise are tables, and the tables have a shape. The permissible noise exposures of the United States regulation on occupational noise, its Table G-16, give the hours a worker may spend at a sound level, in dBA, the A-weighted decibels that the library takes as given: 8 hours at 90, 6 at 92, 4 at 95, 3 at 97, 2 at 100, an hour and a half at 102, 1 at 105, half an hour at 110 and a quarter of an hour or less at 115. Each rise of 5 decibels in that table halves the time, with a rounding here and there, and the number 5 is what the practice calls the exchange rate. The National Institute for Occupational Safety and Health, NIOSH, recommends that exposures be controlled below a level equivalent to 85 dBA for eight hours, and its page states the recommended exposure limit as 85 dBA averaged over an eight-hour workday. The article on noise-induced hearing loss adds that NIOSH recommends a 3 dBA exchange rate, so that every increase of 3 dBA halves the allowed time, where the regulation of OSHA uses 5.

So a limit is two numbers, a reference level for eight hours and an exchange rate, and the library takes yours. The hours it permits at a level are eight, halved once for each exchange rate by which the level exceeds the reference:

d = level - ref_level
return 8.0 / (2.0 ** (d / exchange_rate))

This formula is the library's own derivation from a reference level, eight hours and an exchange rate. It is not a formula read from NIOSH or from OSHA, and the regulation prints its table, not a formula. The program below runs it at 90 and 5 for the levels of the table, with the hours rounded to two places beside the exact ones, and at 85 and 3 for four levels:

import sound

for level in [90.0, 92.0, 95.0, 97.0, 100.0, 102.0, 105.0, 110.0, 115.0]:
    hours = sound.permissible_hours(level, 90.0, 5.0)
    print(level, hours, round(hours, 2))
for level in [85.0, 88.0, 91.0, 94.0]:
    print(level, sound.permissible_hours(level, 85.0, 3.0))
90.0 8.0 8.0
92.0 6.062866266041593 6.06
95.0 4.0 4.0
97.0 3.0314331330207964 3.03
100.0 2.0 2.0
102.0 1.5157165665103982 1.52
105.0 1.0 1.0
110.0 0.5 0.5
115.0 0.25 0.25
85.0 8.0
88.0 4.0
91.0 2.0
94.0 1.0

At 95, 100, 105, 110 and 115 the derivation gives the table's 4, 2, 1, a half and a quarter exactly, because those levels are whole multiples of the exchange rate above 90. At 92, 97 and 102 it gives 6.06, 3.03 and 1.52 hours where the table has 6, 3 and 1.5: the table is rounded where the formula is not. The library says so rather than bend its formula to the table, and a reader who needs the regulation's own figure should read the regulation's. Under the NIOSH pair the allowed time is 8 hours at 85, 4 at 88, 2 at 91 and 1 at 94: each step of 3 decibels halves it, where the table's halves with each step of 5. The same level of 94 decibels is an hour under one pair and, under the OSHA pair, a little over four and a half.

2. Dose, and what "at the limit" is

The dose of an exposure is the hours it lasts over the hours its limit permits. The appendix of the regulation puts it in percent, D = 100 C/T, where C is the length of the exposure and T the reference duration for its level. The library keeps the fraction, and a rule concludes the exposure over its limit when the fraction is more than one:

dose: real = hours / allowed
@rule
def over_limit():
    for e in Exposure:
        if e.dose > 1.0:
            e.over = True

Three exposures show the rule, and the edge of it. A lathe at 95 decibels for four hours is judged against the NIOSH pair, 85 and 3, and a press at the same level for the same time against the OSHA pair, 90 and 5. A saw at 90 decibels for the full eight hours is exactly the whole of what the OSHA pair permits:

import sound

ours = ["lathe", "press", "saw"]
sound.exposure("lathe", 95.0, 4.0, 85.0, 3.0)
sound.exposure("press", 95.0, 4.0, 90.0, 5.0)
sound.exposure("saw", 90.0, 8.0, 90.0, 5.0)
print([n for n in sound.over_limit_names() if n in ours])
print(sound.dose("lathe"), sound.dose("press"), sound.dose("saw"))
n = why_not(sound.Exposure.saw.over)
print(n)
sound.exposure("lathe", 95.0, 0.5, 85.0, 3.0)
print([n for n in sound.over_limit_names() if n in ours], sound.dose("lathe"))
['lathe']
5.039684199579493 1.0 1.0
rule `sound.over_limit` needs sound.Exposure[saw].dose > 1.0, and sound.Exposure[saw].dose is 1.0 (real)
[] 0.6299605249474366

The same four hours at the same 95 decibels are five times the permitted exposure under one limit and exactly the permitted exposure under the other. Nothing in the sound changed. The limit did, and the library therefore keeps it as part of the fact. The saw is at its limit and not over it, and why_not says that the dose is 1.0, which is not more than 1.0. The last pair is a correction: the lathe is written again with half an hour and its mark is withdrawn, because the mark rested on the hours that were replaced. An exposure over its limit is marked and kept, never dropped. It is a valid fact, and over is the only consequence.

3. A day is the sum of its segments

A day is rarely one level. The regulation says of mixed levels that their combined effect should be considered, rather than the individual effect of each, and that if the sum of the fractions C1/T1 + C2/T2 and so on to Cn/Tn exceeds unity, the mixed exposure should be considered to exceed the limit value, Cn being the time at a level and Tn the time permitted at it. The appendix writes the same sum as a dose in percent, D = 100 (C1/T1 + C2/T2 + Cn/Tn). The library's rule is that sentence, the rule for a worker's day:

@rule
def day_total():
    for w in Worker, s in Segment:
        if s.worker == w and absent(w.disputed):
            w.total = sum(s.dose)

The last premise holds the total back while a segment is bound to the day twice, which section 4 comes to.

Each segment carries the hours its own limit permits, so the segments of one day may be judged against different limits, and a day with no segment has no day. Here are three days. The first is a worker with two segments under the NIOSH pair, one of two hours at 95 and one of three hours at 88. The second is a day under the OSHA pair of a hundred decibels for two hours and ninety for four. The third is a day of two hours at 85 and two hours at 82 under the NIOSH pair:

import sound

ours = ["ann", "bo", "cy"]
sound.segment("ann", "a1", 95.0, 2.0, 85.0, 3.0)
sound.segment("ann", "a2", 88.0, 3.0, 85.0, 3.0)
sound.segment("bo", "b1", 100.0, 2.0, 90.0, 5.0)
sound.segment("bo", "b2", 90.0, 4.0, 90.0, 5.0)
sound.segment("cy", "c1", 85.0, 2.0, 85.0, 3.0)
sound.segment("cy", "c2", 82.0, 2.0, 85.0, 3.0)
print([n for n in sound.days_over() if n in ours])
print(sound.total_of("ann"), sound.total_of("bo"), sound.total_of("cy"))
['ann', 'bo']
3.2698420997897464 1.5 0.375

The first day is over by a wide margin: the two hours at 95 alone are 2.52 times what the NIOSH pair permits at that level. The second is over by half, 1.5 and no more, and it is the arithmetic of the regulation, not the arithmetic of the loudest segment: two hours at 100 decibels is exactly the day's whole permission under the OSHA pair, and four hours at 90 add half again. The third day is a quarter from its first segment, since two hours at the reference level are a quarter of eight, and an eighth from its second, since at 82 decibels the pair permits sixteen hours. Its sum is 0.375, below one, so the day is not over. A lower level adds less, but it adds.

4. When two sources disagree

Two sources can say different things about one fact: a time sheet that says a segment lasted two hours, and a dosimeter that says six. The library keeps both readings and does not choose. A reading is written under a second source, beside the first, and every value that is in force gives its own verdict. unsettled() names every fact you write that two sources give different values, and the readers of names read every value in force. A day's total is the exception. Two segments of two hours at eight allowed are a quarter each, and the day is 0.5. Then the first segment's hours are written again as six under a second source:

import sound

ours = ["shift"]
segs = ["m1", "m2"]
sound.segment("shift", "m1", 85.0, 2.0, 85.0, 3.0)
sound.segment("shift", "m2", 85.0, 2.0, 85.0, 3.0)
print(sound.total_of("shift"), [n for n in sound.days_over() if n in ours], [s for s in sound.unsettled() if s.split(" ")[1] in segs])
update(sound.Segment.m1.hours, 6.0, "second reading")
print([n for n in sound.days_over() if n in ours], [s for s in sound.unsettled() if s.split(" ")[1] in segs], [n for n in sound.disputed_days() if n in ours])
doses = list_values(sound.Segment.dose)
for v in doses:
    if v.path.split(".")[2] in segs:
        print(v.path, v.value)
try:
    print(sound.total_of("shift"))
except refuse as r:
    print(r.reason)
0.5 [] []
[] ['segment m1 hours'] ['shift']
sound.Segment.m1.dose 0.25
sound.Segment.m1.dose 0.75
sound.Segment.m2.dose 0.25
day shift is disputed: a segment of it is bound to it twice; forget the claim that is not to stand

Either source alone gives a day that is not over: 0.5 by the time sheet, and by the second reading a segment of 0.75 and another of 0.25, which is exactly one, and a day at its limit is not over it. Together they give the first segment two doses, 0.25 and 0.75, and a sum of them would make the day 1.25, over the limit, a figure neither source gave. So the library does not add them. While a segment is bound to its day twice, the day is disputed: it has no total and is not over, disputed_days() names it, and total_of refuses it and says what to do. unsettled() names the segment, segment m1 hours, so that a reader can go and settle which reading is true. A day that two readings would put over the limit is a prompt to settle the fact, not a finding.

The reader of an exposure's dose refuses for another reason. dose reads a single value with current(), which refuses a cell that holds two different values rather than choose one, and the dose of an exposure is a cell that holds two when the hours of the exposure do:

import sound

ours = ["mill"]
sound.exposure("mill", 90.0, 2.0, 90.0, 5.0)
print(sound.dose("mill"), [n for n in sound.over_limit_names() if n in ours])
update(sound.Exposure.mill.hours, 10.0, "second reading")
print([n for n in sound.over_limit_names() if n in ours], [s for s in sound.unsettled() if s.split(" ")[1] in ours])
try:
    d = sound.dose("mill")
    print(d)
except refuse as r:
    print(r)
0.25 []
['mill'] ['exposure mill hours']
refuse: cell holds 2 competing values — the single-claim read refuses; use `get` to read the full claim set

With one reading, the mill is at a quarter of its limit and not over. With the second, the list reader names it, because one of its values puts it over, and unsettled() names the hours. dose refuses, and says that the cell holds two competing values and that the single-claim read refuses. A refusal is the right answer for a function that returns one number when there are two.

5. The range of hearing

The commonly stated range of human hearing is 20 to 20,000 Hz, and the article that states it describes no formal cut-offs for it. Under ideal laboratory conditions humans can hear a sound as low as 12 Hz and as high as 28 kHz, and the range shrinks during life, usually beginning at around the age of eight with the upper frequency limit being reduced; a gradual loss of sensitivity to higher frequencies with age is considered normal. Other animals have other ranges. The article gives dogs as usually around 67 Hz to 45 kHz, elephants as 16 Hz to 12 kHz, some whales as able to hear infrasonic sounds as low as 7 Hz, the average pigeon as able to hear sounds as low as 0.5 Hz, and bats and dolphins as able to hear frequencies over 100 kHz. The names for what lies outside the human range are the library's: it uses infrasound for what lies below 20 Hz and ultrasound for what lies above 20 kHz, because the page it reads gives no formal cut-offs for them. The three rules say it with both ends of the range inside:

@rule
def audible_tone():
    for t in Tone:
        if t.hz >= 20.0 and t.hz <= 20000.0:
            t.audible = True
import sound

for name, hz in [("below", 19.9), ("low-edge", 20.0), ("middle", 440.0), ("high-edge", 20000.0), ("above", 20000.1)]:
    sound.tone(name, hz)
    print(name, hz, sound.range_of(name))
below 19.9 infrasound
low-edge 20.0 audible
middle 440.0 audible
high-edge 20000.0 audible
above 20000.1 ultrasound

A frequency of 20 Hz is audible and one of 19.9 is infrasound, and the same at 20,000. The edge is the library's convention and the program is exact about it. A person's own range is another matter, and the library cannot say whose ear a tone meets.

6. The arithmetic of sound

A level in decibels is a ratio. For a power quantity it is ten times the logarithm of the ratio, and for a root-power quantity, which a sound pressure is, it is twenty times, because the intensity is proportional to the square of the pressure. The reference for sound in air is 20 micropascals, and 0 dB SPL is approximately the quietest sound a human can hear. An RMS pressure of one pascal corresponds to a level of 94 dB SPL in air. Two equal sources have twice the power of one, and a change in power ratio by a factor of two is a change of 3.0103 decibels, usually rounded to 3. The library gives the level of a pressure as 20.0 * math.log10(pascals / reference_pascals) and the level of two incoherent sources, those whose powers add, as ten times the logarithm of the sum of their powers. The sound pressure of a spherical wavefront radiating from a point source decreases by 50% as the distance is doubled, and, measured in decibels, the decrease is 6.02 dB, since a decibel represents an intensity ratio, while the intensity itself follows the inverse square. The inverse-square law was stated before it was applied to sound: Kepler argued in 1604 that the spreading of light from a point source obeys it, and Newton derived it for gravity in his Principia of 1686.

q = pascals / reference_pascals
return 20.0 * math.log10(q)
return louder + 10.0 * math.log10(1.0 + 10.0 ** ((quieter - louder) / 10.0))
q = r2 / r1
return level_at_r1 - 20.0 * math.log10(q)

A pressure or a distance of zero or below has no level, and the library refuses it. Where a ratio would pass the largest real or fall below the smallest, it takes the difference of the two logarithms instead, which is the same level.

import sound

print(sound.level_of_pressure(1.0, 0.00002), sound.level_of_pressure(0.00002, 0.00002), sound.level_of_pressure(0.2, 0.00002))
print(sound.add_levels(80.0, 80.0), sound.add_levels(80.0, 70.0), sound.add_levels(90.0, 60.0))
print(sound.at_distance(90.0, 1.0, 2.0), sound.at_distance(90.0, 1.0, 10.0))
93.97940008672037 0.0 80.0
83.01029995663981 80.41392685158225 90.00434077479319
83.97940008672037 70.0

One pascal is 93.98 decibels, which the page rounds to 94, and the reference pressure is 0. Ten times the reference pressure is twenty decibels more, and ten thousand times, with a pressure of 0.2 pascal, is 80. Two sources of 80 decibels give 83.01, three decibels more and not a doubling of the number. A source of 80 and one of 70 give 80.41, scarcely more than the louder; and the second of a pair 30 decibels apart adds a few thousandths of a decibel, so that a quiet source beside a loud one is lost in it. A source of 90 decibels at one metre is 84 at two, a fall of 6.02, and 70 at ten, a fall of 20. A zero pressure has no level and refuses, as a distance of zero does.

The speed of sound is the other classical constant of the subject, and it depends on the air. The speed in dry air is about 343 m/s at 20 degrees Celsius and about 331 m/s at 0, and the formula the library uses for a Celsius temperature t is 331.32 times the square root of 1 + t/273.15. Its history is a history of measuring. Mersenne found 448 m/s in 1630, and in 1738 the Académie des Sciences found that, with no wind, the speed at 0 degrees was 332 m/s. Newton's Principia of 1687 includes a computation of the speed in air as 979 feet a second, 298 m/s, too low by about 15%: he had used isothermal and not adiabatic assumptions, and Pierre-Simon Laplace explained the discrepancy, with a heat-capacity ratio for air that produced a near agreement between theory and experiment. In water, Colladon and Sturm found 1437.8 m/s in Lake Geneva in 1826, at 8 degrees; fresh water at 20 degrees carries sound at about 1481, and an experiment on a steel measured 5,930. The wavelength is the speed over the frequency, and the library gives it as such. The Doppler effect completes the set. Doppler first proposed it in 1842, in a treatise on the coloured light of double stars, and Buys Ballot tested it for sound in 1845, finding the pitch higher than the emitted frequency as the source approached and lower as it receded. With the speed of sound c, the receiver's speed toward the source and the source's speed toward the receiver, the library's observed frequency is the source's times (c plus the first) over (c minus the second), each negative for motion away.

return 331.32 * math.sqrt(1.0 + celsius / 273.15)
num = speed + receiver_toward
den = speed - source_toward
v = source_hz * num / den
import sound

print(sound.speed_of_sound(0.0), sound.speed_of_sound(20.0))
c = sound.speed_of_sound(20.0)
print(sound.wavelength(c, 20.0), sound.wavelength(c, 440.0), sound.wavelength(c, 20000.0))
print(sound.doppler(440.0, c, 0.0, 30.0), sound.doppler(440.0, c, 0.0, -30.0))
331.32 343.2353419474446
17.16176709737223 0.7800803226078287 0.01716176709737223
482.14083863568226 404.6335742721313

At 0 degrees the formula gives the constant itself, 331.32. At 20 it gives 343.24. The wavelengths bound the range of hearing: a tone of 20 Hz is 17 metres long in air, a tone of 440 Hz is 0.78 metres, and a tone of 20 kHz is 1.7 centimetres. A source of 440 Hz coming at a listener at 30 metres a second is heard at 482 Hz and, going away, at 405. A source moving at the speed of sound has no answer, and the library refuses with a division by zero; the page on the effect says that the equation is inapplicable for a source that moves toward a stationary observer at or above the speed of the wave.

The last script closes the loop with the first section. The appendix of the regulation gives the 8-hour time-weighted average level for a dose D in percent as TWA = 16.61 log10(D/100) + 90, and the library has it as such:

return 16.61 * math.log10(dose_percent / 100.0) + 90.0
import sound

print(sound.twa_from_dose(100.0), sound.twa_from_dose(50.0), sound.twa_from_dose(91.0), sound.twa_from_dose(200.0))
90.0 84.99989177202127 89.31967752645336 95.00010822797873

A dose of 100 percent is a TWA of 90, the limit. The appendix says that a dose of 91 percent over an eight-hour day gives a TWA of 89.3 dB and a dose of 50 percent corresponds to 85 dB; the first the formula gives as 89.32, and the second as 84.9999, not 85 exactly, because 16.61 is the constant 5 over the logarithm of 2, which is 16.6096..., rounded to two places. A dose of 200 percent is a TWA of 95. This is the inverse of the hours table: every doubling of the dose adds five decibels, the exchange rate. The logarithm of a decibel is correctly rounded here, and CPython's can differ from it by one unit in the last place at some arguments; the level of a pressure of 1.4142135623730951 against a reference of 1 is 3.0102999566398125 here and 3.010299956639812 in CPython 3.11. A decibel agrees with CPython's to that unit, not always to the last bit.

7. What the library leaves out

The library takes a level in A-weighted decibels as given and never computes it from a spectrum. It has no frequency weighting, no octave bands, no room acoustics or reverberation, and no sound in water beyond the figures in this essay's history. It has no action level and no hearing conservation programme, and no limit of another country: the regulation's action level, an 8-hour time-weighted average of 85 decibels, and its separate limit for impulsive or impact noise, which should not exceed 140 dB peak sound pressure level, are both outside it. The exchange rate is a choice the limit makes, and the library takes yours. A dose over a limit says that the hours used more than the limit permits, and nothing about a person. Occupational hearing loss, NIOSH says on its page, is permanent but preventable; the page also says that workers who are exposed to noise at or above its recommended limit are at risk of developing significant hearing loss over their working lifetime. The library can say whether a day was over, and the rest is for a measurement, a programme and a professional.

Closing

The history of sound measurement is a history of putting a number on a thing that is felt. The Bell System made a unit of a logarithm; Newton computed the speed of sound and was fifteen percent wrong, and Laplace found why; Doppler wrote of double stars and Buys Ballot tested the idea with sound. The occupational limits are the same kind of act: a table that halves the time for each rise of five decibels, or of three. The library keeps these procedures where a reader can run them and ask about them. It names the premise that failed when a day is not over, keeps a dose at exactly one as at the limit, holds two readings of a fact side by side and names the fact they disagree about, and refuses where there is no value to give. And because each conclusion names the facts it rests on, an exposure corrected in one place is corrected in every verdict that rested on it.

Free to read

William Moebs, Samuel J. Ling and Jeff Sanny, University Physics, Volume 1 (OpenStax, Rice University, Houston, 2016). Chapter 17, “Sound”, treats sound intensity and the decibel. Read it free at OpenStax.

Read on