voice_preview_generator.gd 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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, 0, 0, 0)
  9. var foreground_color
  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. #set colour based on theme
  18. var interface_settings = ConfigHandler.load_interface_settings()
  19. #check if the theme is inverted
  20. if interface_settings.invert_theme:
  21. foreground_color = Color(0.102, 0.102, 0.102, 0.6)
  22. else:
  23. foreground_color = Color(0.898, 0.898, 0.898, 0.6)
  24. if not stream:
  25. return
  26. if stream.format == AudioStreamWAV.FORMAT_IMA_ADPCM:
  27. return
  28. if image_max_width <= 0:
  29. return
  30. # If already working, abort previous job first.
  31. if is_working:
  32. must_abort = true
  33. while is_working:
  34. await get_tree().process_frame
  35. is_working = true
  36. var data: PackedByteArray = stream.data
  37. var data_size: int = data.size()
  38. var is_stereo: bool = stream.stereo
  39. var is_16bit: bool = (stream.format == AudioStreamWAV.FORMAT_16_BITS)
  40. # Assume non-ADPCM, non-16-bit is 24-bit PCM for our preview purposes
  41. var bytes_per_sample: int = 2 if is_16bit else 3
  42. var channels: int = 2 if is_stereo else 1
  43. var frame_bytes: int = bytes_per_sample * channels
  44. if frame_bytes <= 0:
  45. is_working = false
  46. return
  47. var total_frames: int = int(floor(data_size / frame_bytes))
  48. if total_frames <= 0:
  49. is_working = false
  50. return
  51. # Decide how many frames contribute to each pixel column
  52. var frames_per_pixel: int = int(ceil(total_frames / float(image_max_width)))
  53. var img_width: int = int(floor(total_frames / float(frames_per_pixel)))
  54. if img_width <= 0:
  55. img_width = 1
  56. var img := Image.create(img_width, IMAGE_HEIGHT, true, Image.FORMAT_RGBA8)
  57. img.fill(background_color)
  58. # For speed, sample only a subset of frames in each pixel column.
  59. # Tweak this to trade accuracy for speed (e.g., 8 or 12 samples per column).
  60. var samples_per_pixel_target: int = 4
  61. var inner_step: int = max(1, int(floor(frames_per_pixel / float(samples_per_pixel_target))))
  62. var x: int = 0
  63. var frames_processed: int = 0
  64. while x < img_width:
  65. var start_frame: int = x * frames_per_pixel
  66. var end_frame: int = min(start_frame + frames_per_pixel, total_frames)
  67. var min_l := 128
  68. var max_l := 128
  69. var min_r := 128
  70. var max_r := 128
  71. var f: int = start_frame
  72. while f < end_frame:
  73. var base := f * frame_bytes
  74. # ---- Decode LEFT -> fast 8-bit signed, then map to 0..255
  75. var l_u8: int
  76. if is_16bit:
  77. var l_lo := data[base]
  78. var l_hi := data[base + 1]
  79. var l16 := (l_hi << 8) | l_lo
  80. if (l_hi & 0x80) != 0:
  81. l16 -= 0x10000
  82. l_u8 = ((l16 >> 8) + 128)
  83. else:
  84. var l_b0 := data[base]
  85. var l_b1 := data[base + 1]
  86. var l_b2 := data[base + 2]
  87. var l24 := (l_b2 << 16) | (l_b1 << 8) | l_b0
  88. if (l_b2 & 0x80) != 0:
  89. l24 -= 0x1000000
  90. l_u8 = ((l24 >> 16) + 128)
  91. l_u8 = clamp(l_u8, 0, 255)
  92. # ---- Decode RIGHT (or mirror left for mono)
  93. var r_u8: int = l_u8
  94. if is_stereo:
  95. var ro := bytes_per_sample # right channel offset inside frame
  96. if is_16bit:
  97. var r_lo := data[base + ro]
  98. var r_hi := data[base + ro + 1]
  99. var r16 := (r_hi << 8) | r_lo
  100. if (r_hi & 0x80) != 0:
  101. r16 -= 0x10000
  102. r_u8 = ((r16 >> 8) + 128)
  103. else:
  104. var r_b0 := data[base + ro]
  105. var r_b1 := data[base + ro + 1]
  106. var r_b2 := data[base + ro + 2]
  107. var r24 := (r_b2 << 16) | (r_b1 << 8) | r_b0
  108. if (r_b2 & 0x80) != 0:
  109. r24 -= 0x1000000
  110. r_u8 = ((r24 >> 16) + 128)
  111. r_u8 = clamp(r_u8, 0, 255)
  112. # Update min/max per channel
  113. if l_u8 < min_l: min_l = l_u8
  114. if l_u8 > max_l: max_l = l_u8
  115. if r_u8 < min_r: min_r = r_u8
  116. if r_u8 > max_r: max_r = r_u8
  117. f += inner_step
  118. frames_processed += inner_step
  119. if must_abort:
  120. is_working = false
  121. must_abort = false
  122. return
  123. # Draw column
  124. if is_stereo:
  125. _draw_half_waveform(img, x, min_l, max_l, 0, IMAGE_HEIGHT / 2)
  126. _draw_half_waveform(img, x, min_r, max_r, IMAGE_HEIGHT / 2, IMAGE_HEIGHT / 2)
  127. else:
  128. _draw_half_waveform(img, x, min_l, max_l, 0, IMAGE_HEIGHT)
  129. x += 1
  130. # Lightweight progress update
  131. if (x % 16) == 0:
  132. var progress := float(x) / float(img_width)
  133. emit_signal("generation_progress", progress)
  134. await get_tree().process_frame
  135. is_working = false
  136. emit_signal("texture_ready", ImageTexture.create_from_image(img))
  137. func _draw_half_waveform(img: Image, x: int, min_val: int, max_val: int, y_offset: int, draw_height: int):
  138. var scale = draw_height / 256.0
  139. var center_y = y_offset + int(draw_height / 2)
  140. var min_y = int(center_y - (max_val - 128) * scale)
  141. var max_y = int(center_y - (min_val - 128) * scale)
  142. min_y = clamp(min_y, y_offset, y_offset + draw_height - 1)
  143. max_y = clamp(max_y, y_offset, y_offset + draw_height - 1)
  144. for y in range(min_y, max_y + 1):
  145. img.set_pixel(x, y, foreground_color)
  146. func _reset_to_blank():
  147. var img = Image.create(1, IMAGE_HEIGHT, true, Image.FORMAT_RGBA8)
  148. img.fill(Color.DARK_SLATE_GRAY)
  149. emit_signal("texture_ready", ImageTexture.create_from_image(img))