Download the file
  1. #!/usr/bin/python
  2.  
  3. # Copyright (C) 2007 Matthieu Weber <mweber@mit.jyu.fi>
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or (at
  8. # your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  18. # USA
  19.  
  20. ###############################################################################
  21. # This program is meant to measure the sensitivity one one person's ears.
  22. # Simply connect earphones to the soundcard, put them on, start the program
  23. # and press the space key whenever you hear a sound.
  24. #
  25. # It generates a datafile that can be plotted in Gnuplot in order to get an
  26. # audiogram. It will not provide an accurate audiogram, since for that purpose
  27. # it would be necessary to calibrate the soundlevel of the computer and of the
  28. # headphones, but it allows to compare two persons hearing, or the sensitivity
  29. # of one person's hearing across a set of frequencies.
  30.  
  31. ###############################################################################
  32. # Begin Configuration
  33.  
  34. # sampling rate (Hz)
  35. rate = 32000
  36.  
  37. # maximum duration of one beep (s)
  38. duration = 2
  39.  
  40. # frequency range (Hz)
  41. freq_range = [125, 250, 500, 1000, 2000, 4000, 8000, 10000, 12000, 14000, 16000]
  42.  
  43. # Loudness range (Phon)
  44. loud_range = [0, 5, 10, 15, 20, 25, 30, 35, 40]
  45.  
  46. # End Configuration
  47. ###############################################################################
  48.  
  49. # Equal Loudness contour
  50. contour = [
  51. # 0 Phon -> spl
  52. [25, 13, 8, 5, 4, 3, 2, 2, 2, 2, -2, -7, -6, -1, 4, 8, 11, 13, 14, 14],
  53. # 20 Phon -> spl
  54. [46, 33, 29, 25, 23, 22, 21, 20, 20, 20, 18, 15, 15, 18, 23, 28, 31, 33, 35, 35],
  55. # 40 Phon -> spl
  56. [62, 51, 46, 43, 42, 41, 40, 40, 40, 40, 39, 36, 36, 40, 45, 48, 52, 54, 55, 55]
  57. ]
  58.  
  59. side = ['Right', 'Left']
  60.  
  61. import struct
  62. import ossaudiodev
  63. import math
  64. import termios, fcntl, sys, os
  65. import time
  66. import random
  67.  
  68. ################################################################################
  69. #
  70. # User Interface
  71. #
  72. ################################################################################
  73.  
  74. class UI:
  75. def __init__(self):
  76. self.__fd = None
  77. self.__oldflags = None
  78. self.__oldterm = None
  79. self.init()
  80.  
  81. def init(self):
  82. self.__fd = sys.stdin.fileno()
  83. self.__oldterm = termios.tcgetattr(self.__fd)
  84. newattr = termios.tcgetattr(self.__fd)
  85. newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
  86. termios.tcsetattr(self.__fd, termios.TCSANOW, newattr)
  87. self.__oldflags = fcntl.fcntl(self.__fd, fcntl.F_GETFL)
  88. fcntl.fcntl(self.__fd, fcntl.F_SETFL, self.__oldflags | os.O_NONBLOCK)
  89.  
  90. def close(self):
  91. termios.tcsetattr(self.__fd, termios.TCSAFLUSH, self.__oldterm)
  92. fcntl.fcntl(self.__fd, fcntl.F_SETFL, self.__oldflags)
  93.  
  94. def get_key(self):
  95. c = ""
  96. try:
  97. c = sys.stdin.read(1)
  98. except IOError:
  99. pass
  100. return c
  101.  
  102. def flush_key(self):
  103. while True:
  104. try:
  105. sys.stdin.read(1)
  106. except IOError:
  107. break
  108.  
  109. def progress_bar(self, value, max, width, prefix="", suffix=""):
  110. n = int(math.floor(float(value)*float(width)/float(max) + .5))
  111. s = "%s[" % prefix
  112. s += n * "-"
  113. s += (width - n) * " "
  114. s += "] %d%s\r" % (value, suffix)
  115. sys.stdout.write(s)
  116.  
  117. ################################################################################
  118. #
  119. # Audio
  120. #
  121. ################################################################################
  122.  
  123. class Audio:
  124. def __init__(self):
  125. self.__audio = None
  126. self.__t0 = 0
  127. self.threshold = [0, 0]
  128. self.init()
  129.  
  130. def init(self):
  131. self.__audio = ossaudiodev.open('w')
  132. self.__audio.setparameters(ossaudiodev.AFMT_S16_LE, 2, rate)
  133.  
  134. def close(self):
  135. self.__audio.reset()
  136. self.__audio.close()
  137.  
  138. def reset(self):
  139. self.__audio.reset()
  140. #self.__audio.sync()
  141. #self.close()
  142. #self.init()
  143. self.__t0 = 0
  144.  
  145. def pause(self, ch, duration):
  146. self.play(0, 100, ch, duration)
  147.  
  148. def loudness2amplitude(self, loud, freq, ch):
  149. if loud > 40:
  150. sys.exit("Loudness cannot be more than 40 phons")
  151. # We calculate a linear interpolation of the values in "contour"
  152. f = 0
  153. ffrac = 0
  154. if freq < 1000:
  155. f = int(math.floor(freq / 100.0) - 1)
  156. ffrac = freq/100.0 - math.floor(freq / 100.0)
  157. elif freq < 10000:
  158. f = int(math.floor(freq / 1000.0) + 8)
  159. ffrac = freq/1000.0 - math.floor(freq / 1000.0)
  160. else:
  161. f = 18
  162. if ffrac < 0.0001:
  163. ffrac = 0
  164. if ffrac > 0.9999:
  165. ffrac = 0
  166. f += 1
  167.  
  168. l = int(math.floor(loud/20.0))
  169. lfrac = loud/20.0 - math.floor(loud/20.0)
  170. if lfrac < 0.0001:
  171. lfrac = 0
  172. if lfrac > 0.9999:
  173. lfrac = 0
  174. l += 1
  175.  
  176. a = b = c = d = contour[l][f]
  177. if f < 18:
  178. b = contour[l][f+1]
  179. d = b
  180. if l < 2:
  181. c = contour[l+1][f]
  182. d = contour[l+1][f+1]
  183. x = a + ffrac * (b - a)
  184. y = c + ffrac * (d - c)
  185. z = x + lfrac * (y - x)
  186. return self.threshold[ch] * 10**(z/20.0)
  187.  
  188. def play (self, amplitude, freq, chan, duration):
  189. ch = [0, 0]
  190. t = 0
  191. while t < duration * rate:
  192. ch[chan] = math.sin(freq * 2 * math.pi * (self.__t0 + t) / rate) * amplitude
  193. self.__audio.write(struct.pack('hh', ch[0], ch[1]))
  194. t += 1
  195. self.__t0 += t
  196.  
  197. ################################################################################
  198. #
  199. # Controller
  200. #
  201. ################################################################################
  202.  
  203. class Controller:
  204. def __init__(self):
  205. if len(sys.argv) < 2:
  206. sys.exit("Usage: %s filename\n" % sys.argv[0])
  207. self.__outfilename = sys.argv[1]
  208. self.__audio = Audio()
  209. self.__ui = UI()
  210. self.step = 0.1
  211. self.results = {}
  212.  
  213. def check_key(self, key, chan):
  214. if (key != ''):
  215. return True
  216. return False
  217.  
  218. def test(self, loud, freq, chan, duration):
  219. t = 0
  220. k = ''
  221. ampl = self.__audio.loudness2amplitude(loud, freq, chan)
  222. if ampl > 32767:
  223. return False
  224. while t < duration:
  225. self.__audio.play(ampl, freq, chan, self.step)
  226. k = self.__ui.get_key()
  227. if k != '':
  228. return self.check_key(k, chan)
  229. t += self.step
  230. return False
  231.  
  232. def calibrate(self):
  233. print "Calibrating..."
  234. print "Press '+' to increase, '-' to decrease, any other key to set the calibration value"
  235. for c in [0, 1]:
  236. self.__audio.threshold[c] = 1
  237. # 2319 = threshold for 0 Phon @ 100Hz
  238. while self.__audio.threshold[c] < 2319:
  239. t = 0
  240. stop = False
  241. self.__ui.progress_bar(20*math.log10(self.__audio.threshold[c]), 20*math.log10(2319), 60, " %5s ear: " % side[c])
  242. while t < 1 / self.step:
  243. self.__audio.play(self.__audio.threshold[c], 1000, 0, self.step)
  244. k = self.__ui.get_key()
  245. if k == '-':
  246. self.__audio.threshold[c] /= 2
  247. break
  248. elif k == '+':
  249. self.__audio.threshold[c] *= 1.414213
  250. break
  251. elif k != '':
  252. stop = True
  253. break
  254. t += 1
  255. if stop:
  256. break
  257. self.__audio.threshold[c] *= 1.414213
  258. self.__audio.reset()
  259. sys.stdout.write("\n")
  260. if self.__audio.threshold[c] > 2319:
  261. sys.exit("Value too high. Calibration failed.")
  262. if self.__audio.threshold[c] > 26:
  263. print "Full dynamic range will not be available"
  264. # Hearing threshold at 1kHz = 2 dB SPL, not 0 dB SPL
  265. self.__audio.threshold[c] = math.floor(0.5 + self.__audio.threshold[c] / 10**0.1)
  266. #print "Threshold = %d" % self.__audio.threshold[c]
  267.  
  268.  
  269. def close(self):
  270. self.__audio.close()
  271. self.__ui.close()
  272. print ""
  273.  
  274. def run(self):
  275. print "\n"
  276. time.sleep(2)
  277. for ch in [0, 1]:
  278. print "%s ear" % side[ch]
  279. for freq in freq_range:
  280. if not self.results.has_key(freq):
  281. self.results[freq] = [0,0]
  282. self.__ui.flush_key()
  283. for level in loud_range:
  284. self.__ui.progress_bar(level, loud_range[-1], loud_range[-1], "%5d Hz " % freq, " Phon")
  285. self.results[freq][ch] = level
  286. if self.test(level, freq, ch, duration):
  287. break
  288. self.__audio.reset()
  289. self.__audio.pause(ch, 1)
  290. print ""
  291. print ""
  292.  
  293. def __str__(self):
  294. s = "# Audiogramme %s\n# freq L R\n" % time.strftime("%Y-%m-%d %H:%M:%S")
  295. sorted_freq = self.results.keys()
  296. sorted_freq.sort()
  297. for freq in sorted_freq:
  298. ch = self.results[freq]
  299. s += "%d %d %d\n" % (freq, -ch[0], -ch[1])
  300. return s
  301.  
  302. def save(self):
  303. outfile = file(self.__outfilename, "w")
  304. outfile.write(str(self))
  305. outfile.close()
  306.  
  307. ###############################################################################
  308.  
  309. controller = Controller()
  310.  
  311. try:
  312. try:
  313. controller.calibrate()
  314. controller.run()
  315. controller.save()
  316. except KeyboardInterrupt:
  317. pass
  318. finally:
  319. controller.close()
  320.