voice_preview_generator.gd 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. @tool
  2. extends Node
  3. signal texture_ready(texture)
  4. signal generation_progress(normalized_progress)
  5. const MAX_FREQUENCY: float = 3000.0 # Maximum frequency captured
  6. const IMAGE_HEIGHT: int = 64
  7. var image_compression: float = 10.0 # How many samples in one pixel
  8. var background_color = Color(0.2, 0.2, 0.4, 0.5)
  9. var foreground_color = Color.SILVER
  10. # =============================================================================
  11. const SAMPLING_RATE = 2.0*MAX_FREQUENCY
  12. const IMAGE_HEIGHT_FACTOR: float = float(IMAGE_HEIGHT) / 256.0 # Converts sample raw height to pixel
  13. const IMAGE_CENTER_Y = int(round(IMAGE_HEIGHT / 2.0))
  14. var is_working := false
  15. var must_abort := false
  16. func generate_preview(stream: AudioStreamWAV, image_max_width: int = 500):
  17. if not stream:
  18. return
  19. if stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:
  20. return # not supported
  21. if image_max_width <= 0:
  22. return # User wasn't remarkably brilliant
  23. if is_working:
  24. must_abort = true
  25. while is_working:
  26. await get_tree().process_frame
  27. is_working = true
  28. var data = stream.data
  29. var data_size = data.size()
  30. var is_16bit = (stream.format == AudioStreamWAV.FORMAT_16_BITS)
  31. var is_stereo = stream.stereo
  32. # For display reasons, lower frequencies than the sampling rate might suffice.
  33. # According to the gentlemen of noble steem known as Nyquist and Shannon,
  34. # we can sample at SAMPLING_RATE
  35. var sample_interval = 1
  36. if stream.mix_rate > SAMPLING_RATE:
  37. sample_interval = int(round(stream.mix_rate / SAMPLING_RATE))
  38. if is_16bit:
  39. sample_interval *= 2
  40. if is_stereo:
  41. sample_interval *= 2
  42. var reduced_data = PackedByteArray()
  43. # We use floor(), not round(), because extra elements in the end of data
  44. # before next sampling interval are discarded
  45. var reduced_data_size = int(floor( data_size / float(sample_interval) ))
  46. reduced_data.resize(reduced_data_size)
  47. # For drawing a preview, we use only one byte left channel per sample
  48. # PCM16 is little endian, so MSB is index 1, not 0
  49. # reduced_data will contain only that one byte per sample
  50. var sample_in_i := 1 if is_16bit else 0
  51. var sample_out_i := 0
  52. while (sample_in_i < data_size) and (sample_out_i < reduced_data_size):
  53. reduced_data[sample_out_i] = data[sample_in_i]
  54. sample_in_i += sample_interval
  55. sample_out_i += 1
  56. if must_abort:
  57. is_working = false
  58. must_abort = false
  59. return
  60. # From now on we work only with reduced_data
  61. image_compression = ceil(reduced_data_size / float(image_max_width))
  62. var img_width = floor(reduced_data_size/image_compression) # Again floor as we discard remaining samples
  63. var img = Image.create(img_width, IMAGE_HEIGHT, true, Image.FORMAT_RGBA8)
  64. img.fill(Color.DARK_SLATE_GRAY)
  65. var sample_i = 0
  66. var img_x = 0
  67. var final_sample_i = (reduced_data_size - image_compression)
  68. while sample_i < final_sample_i:
  69. var min_val := 128
  70. var max_val := 128
  71. for block_i in range(image_compression):
  72. var sample_val = reduced_data[sample_i]
  73. # Convert signed bytes to unsigned bytes
  74. sample_val += 128
  75. if sample_val >= 256:
  76. sample_val -= 256
  77. # Get minmax
  78. if sample_val < min_val:
  79. min_val = sample_val
  80. if sample_val > max_val:
  81. max_val = sample_val
  82. sample_i += 1
  83. # Center pixel is always drawn
  84. if (min_val == 128) and (max_val == 128):
  85. img.set_pixel(img_x, IMAGE_CENTER_Y, foreground_color)
  86. else:
  87. var min_height = int(clamp(
  88. floor(IMAGE_HEIGHT - (min_val*IMAGE_HEIGHT_FACTOR)),
  89. 0, IMAGE_HEIGHT-1
  90. ))
  91. var max_height = int(clamp(
  92. floor(IMAGE_HEIGHT - (max_val*IMAGE_HEIGHT_FACTOR)),
  93. 0, IMAGE_HEIGHT-1
  94. ))
  95. # min_height and max_height are in audio sample direction (positive up)
  96. # while img_y is in image direction (positive down)
  97. var img_y = max_height # top value is lower img_y
  98. while img_y <= min_height: # bottom value is higher img_y
  99. img.set_pixel(img_x, img_y, foreground_color)
  100. img_y += 1
  101. img_x += 1
  102. if must_abort:
  103. is_working = false
  104. must_abort = false
  105. return
  106. if (sample_i % 100) == 0:
  107. var progress = sample_i / final_sample_i
  108. emit_signal("generation_progress", progress)
  109. await get_tree().process_frame
  110. is_working = false
  111. emit_signal("texture_ready", ImageTexture.create_from_image(img))