You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
4.2KB

  1. #!/usr/bin/python2.5
  2. #
  3. # Copyright 2013 Olivier Gillet.
  4. #
  5. # Author: Olivier Gillet (ol.gillet@gmail.com)
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. # See http://creativecommons.org/licenses/MIT/ for more information.
  26. #
  27. # -----------------------------------------------------------------------------
  28. #
  29. # Streaming .wav file writer.
  30. import copy
  31. import numpy
  32. import struct
  33. _DATA_CHUNK_HEADER_SIZE = 8
  34. _FMT_CHUNK_DATA_SIZE = 16
  35. _FMT_CHUNK_HEADER_SIZE = 8
  36. _RIFF_FORMAT_DESCRIPTOR_SIZE = 4
  37. _UNSIGNED_CHAR_TO_FLOAT_SCALE = 128.0
  38. _FLOAT_TO_UNSIGNED_CHAR_SCALE = 127.0
  39. class AudioStreamWriter(object):
  40. def __init__(self, file_name, sample_rate, bitdepth=16, num_channels=1):
  41. self._sr = sample_rate
  42. self._file_name = file_name
  43. self._sample_rate = sample_rate
  44. self._bitdepth = bitdepth
  45. self._num_channels = num_channels
  46. self._f = file(file_name, 'wb')
  47. self._num_bytes = 0
  48. self._write_header(False)
  49. @staticmethod
  50. def quantize(signal, bitdepth):
  51. """Convert an array of float to an array of integers.
  52. Args:
  53. signal: numpy array. source signal.
  54. bitdepth: int. size of the integer in bits.
  55. Returns:
  56. array of integers.
  57. """
  58. norm = numpy.abs(signal).max()
  59. # Normalization or clipping.
  60. scaled_signal = copy.copy(signal)
  61. if norm > 1.0:
  62. logging.warning('Some samples will be clipped.')
  63. # Clip samples above 1 and below -1.
  64. scaled_signal[scaled_signal < -1] = -1
  65. scaled_signal[scaled_signal > 1] = 1
  66. if bitdepth == 8:
  67. scaled_signal = (scaled_signal + 1.0) * _FLOAT_TO_UNSIGNED_CHAR_SCALE
  68. scaled_signal = numpy.array(scaled_signal, dtype=numpy.uint8)
  69. else:
  70. scale = (1 << (bitdepth - 1)) - 1
  71. # pylint: disable-msg=C6407
  72. scaled_signal = scaled_signal * scale
  73. scaled_signal = numpy.array(scaled_signal, dtype='i%d' % (bitdepth / 8))
  74. return scaled_signal
  75. def _write_header(self, restore_position):
  76. f = self._f
  77. total_size = _RIFF_FORMAT_DESCRIPTOR_SIZE
  78. total_size += _FMT_CHUNK_HEADER_SIZE + _FMT_CHUNK_DATA_SIZE
  79. total_size += _DATA_CHUNK_HEADER_SIZE + self._num_bytes
  80. current_position = f.tell()
  81. f.seek(0)
  82. f.write('RIFF')
  83. f.write(struct.pack('<L', total_size))
  84. f.write('WAVEfmt ')
  85. bitrate = self._sample_rate * self._num_channels * (self._bitdepth / 8)
  86. bits_per_sample = self._num_channels * (self._bitdepth / 8)
  87. f.write(struct.pack(
  88. '<LHHLLHH',
  89. 16,
  90. 1,
  91. self._num_channels,
  92. self._sample_rate,
  93. bitrate,
  94. bits_per_sample,
  95. self._bitdepth))
  96. f.write('data')
  97. f.write(struct.pack('<L', self._num_bytes))
  98. if restore_position:
  99. f.seek(current_position)
  100. def append(self, signal):
  101. if signal.dtype == numpy.uint8 or signal.dtype == numpy.int16:
  102. assert self._bitdepth == signal.dtype.itemsize * 8
  103. scaled_signal = signal
  104. else:
  105. scaled_signal = self.quantize(signal, self._bitdepth)
  106. if scaled_signal.ndim == 1:
  107. assert self._num_channels == 1
  108. else:
  109. assert self._num_channels == scaled_signal.shape[1]
  110. scaled_signal.tofile(self._f)
  111. self._num_bytes += scaled_signal.nbytes
  112. self._write_header(True)