generate_opus.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env python3
  2. from kaitaistruct import KaitaiStruct, ValidationNotEqualError
  3. import os
  4. import getopt
  5. import sys
  6. import glob
  7. from functools import reduce
  8. class Ogg(KaitaiStruct):
  9. """Ogg is a popular media container format, which provides basic
  10. streaming / buffering mechanisms and is content-agnostic. Most
  11. popular codecs that are used within Ogg streams are Vorbis (thus
  12. making Ogg/Vorbis streams) and Theora (Ogg/Theora).
  13. Ogg stream is a sequence Ogg pages. They can be read sequentially,
  14. or one can jump into arbitrary stream location and scan for "OggS"
  15. sync code to find the beginning of a new Ogg page and continue
  16. decoding the stream contents from that one.
  17. """
  18. def __init__(self, _io, _parent=None, _root=None):
  19. KaitaiStruct.__init__(self, _io)
  20. self._parent = _parent
  21. self._root = _root if _root else self
  22. self._read()
  23. def _read(self):
  24. self.pages = []
  25. i = 0
  26. while not self._io.is_eof():
  27. self.pages.append(Ogg.Page(self._io, self, self._root))
  28. i += 1
  29. class Page(KaitaiStruct):
  30. """Ogg page is a basic unit of data in an Ogg bitstream, usually
  31. it's around 4-8 KB, with a maximum size of 65307 bytes.
  32. """
  33. def __init__(self, _io, _parent=None, _root=None):
  34. KaitaiStruct.__init__(self, _io)
  35. self._parent = _parent
  36. self._root = _root if _root else self
  37. self._read()
  38. def _read(self):
  39. self.sync_code = self._io.read_bytes(4)
  40. if not self.sync_code == b"\x4F\x67\x67\x53":
  41. raise ValidationNotEqualError(b"\x4F\x67\x67\x53", self.sync_code, self._io,
  42. u"/types/page/seq/0")
  43. self.version = self._io.read_bytes(1)
  44. if not self.version == b"\x00":
  45. raise ValidationNotEqualError(b"\x00", self.version, self._io, u"/types/page/seq/1")
  46. self.reserved1 = self._io.read_bits_int_be(5)
  47. self.is_end_of_stream = self._io.read_bits_int_be(1) != 0
  48. self.is_beginning_of_stream = self._io.read_bits_int_be(1) != 0
  49. self.is_continuation = self._io.read_bits_int_be(1) != 0
  50. self._io.align_to_byte()
  51. self.granule_pos = self._io.read_u8le()
  52. self.bitstream_serial = self._io.read_u4le()
  53. self.page_seq_num = self._io.read_u4le()
  54. self.crc32 = self._io.read_u4le()
  55. self.num_segments = self._io.read_u1()
  56. self.len_segments = [None] * self.num_segments
  57. for i in range(self.num_segments):
  58. self.len_segments[i] = self._io.read_u1()
  59. self.segments = [None] * self.num_segments
  60. for i in range(self.num_segments):
  61. self.segments[i] = self._io.read_bytes(self.len_segments[i])
  62. def generate(input_file: str, output_dir: str, max_samples: int):
  63. if output_dir[-1] != "/":
  64. output_dir += "/"
  65. if os.path.isdir(output_dir):
  66. files_to_delete = glob.glob(output_dir + "*.opus")
  67. if len(files_to_delete) > 0:
  68. print("Remove following files?")
  69. for file in files_to_delete:
  70. print(file)
  71. response = input("Remove files? [y/n] ").lower()
  72. if response != "y" and response != "yes":
  73. print("Cancelling...")
  74. return
  75. print("Removing files")
  76. for file in files_to_delete:
  77. os.remove(file)
  78. else:
  79. os.makedirs(output_dir, exist_ok=True)
  80. audio_stream_file = "_audio_stream.ogg"
  81. if os.path.isfile(audio_stream_file):
  82. os.remove(audio_stream_file)
  83. os.system('ffmpeg -i {} -vn -ar 48000 -ac 2 -vbr off -acodec libopus -ab 64k {}'.format(input_file, audio_stream_file))
  84. data = Ogg.from_file(audio_stream_file)
  85. index = 0
  86. valid_pages = data.pages[2:]
  87. segments = list(reduce(lambda x, y: x + y.segments, valid_pages, []))[:max_samples]
  88. for segment in segments:
  89. name = "{}sample-{}.opus".format(output_dir, index)
  90. index += 1
  91. with open(name, 'wb') as file:
  92. assert len(list(segment)) == 160
  93. file.write(segment)
  94. os.remove(audio_stream_file)
  95. def main(argv):
  96. input_file = None
  97. default_output_dir = "opus/"
  98. output_dir = default_output_dir
  99. max_samples = None
  100. try:
  101. opts, args = getopt.getopt(argv, "hi:o:m:", ["help", "ifile=", "odir=", "max="])
  102. except getopt.GetoptError:
  103. print('generate_opus.py -i <input_files> [-o <output_files>] [-m <max_samples>] [-h]')
  104. sys.exit(2)
  105. for opt, arg in opts:
  106. if opt in ("-h", "--help"):
  107. print("Usage: generate_opus.py -i <input_files> [-o <output_files>] [-m <max_samples>] [-h]")
  108. print("Arguments:")
  109. print("\t-i,--ifile: Input file")
  110. print("\t-o,--odir: Output directory (default: " + default_output_dir + ")")
  111. print("\t-m,--max: Maximum generated samples")
  112. print("\t-h,--help: Print this help and exit")
  113. sys.exit()
  114. elif opt in ("-i", "--ifile"):
  115. input_file = arg
  116. elif opt in ("-o", "--ofile"):
  117. output_dir = arg
  118. elif opt in ("-m", "--max"):
  119. max_samples = int(arg)
  120. if input_file is None:
  121. print("Missing argument -i")
  122. sys.exit(2)
  123. generate(input_file, output_dir, max_samples)
  124. if __name__ == "__main__":
  125. main(sys.argv[1:])