Skip to content

Commit cfb9484

Browse files
committed
Refactor demo recorder, add Android export
Refactors demo_scene.gd for clearer structure and Android-safe saving, and improves 24-bit playback buffering. - Split initialization into helpers: platform permission request (_initialize_platform_permissions), UI setup, and processing (_process -> _update_volume_meter_logic). - Add ANDROID_RECORDINGS_SUBDIR constant and implement Android export workflow: save to a temporary user:// file, then copy to Documents/recordings (creating the dir if needed) to work around scoped storage. Uses a short crypto-based id for filenames. - Centralize UI state changes with _toggle_playback_controls and simplify device selection handlers. - Add scene connection for output device selection in demo_scene.tscn. - Improve AudioStreamPlayerWav24B: track when all frames were pushed, avoid hanging by finalizing playback when server buffer empties, reset state on play/seek, and push buffers more robustly. - Add assets/.gdignore and remove two unused .import files (banner.webp.import, icon.png.import). These changes improve Android compatibility, UI responsiveness, and reliability of 24-bit playback.
1 parent 3071d9f commit cfb9484

6 files changed

Lines changed: 96 additions & 65 deletions

File tree

addons/direct_audio_and_wav_24bits/demo/demo_scene.gd

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ extends Control
33
## Example scene demonstrating the 24-bit high-fidelity recording workflow.
44
## This script handles UI updates, recording states, and file management.
55

6+
# --- Constants ---
7+
const ANDROID_RECORDINGS_SUBDIR = "recordings"
8+
69
# --- UI References ---
710
@onready var start_and_stop_button: Button = %StartAndStopButton
811
@onready var play_stop_button: Button = %PlayStopButton
912
@onready var save_button: Button = %SaveButton
1013
@onready var format_option_button: OptionButton = %OptionButton
1114
@onready var volume_progress_bar: ProgressBar = %ProgressBar
15+
1216
@onready var input_hz_label: Label = %InputHz
1317
@onready var output_hz_label: Label = %OutputHz
18+
1419
@onready var input_devices_option: OptionButton = %InputDevicesOption
1520
@onready var output_devices_option: OptionButton = %OutputDevicesOption
1621

17-
1822
# --- Audio References ---
1923
@onready var recorder: DirectAudioInputRecorder = $DirectAudioInputRecorder
2024
@onready var player_24bit: AudioStreamPlayerWav24B = $AudioStreamPlayerWav24B
@@ -31,13 +35,19 @@ var _last_recorded_resource: AudioStreamWAV
3135

3236
func _ready() -> void:
3337

34-
if OS.get_name() == "Android":
35-
OS.request_permissions()
36-
38+
_initialize_platform_permissions()
3739
_setup_ui_initial_state()
3840
_populate_format_options()
3941
_load_devices()
4042

43+
func _process(delta: float) -> void:
44+
_update_volume_meter_logic(delta)
45+
46+
func _initialize_platform_permissions() -> void:
47+
if OS.get_name() == "Android":
48+
# Requesting multiple permissions often needed for audio/storage
49+
OS.request_permissions()
50+
4151
func _setup_ui_initial_state() -> void:
4252
input_hz_label.text = str(AudioServer.get_input_mix_rate())
4353
output_hz_label.text = str(AudioServer.get_mix_rate())
@@ -64,25 +74,16 @@ func _populate_format_options() -> void:
6474

6575
format_option_button.selected = recorder.format
6676

67-
# --- Main Loop ---
77+
# --- Logic & Processing ---
6878

69-
func _process(delta: float) -> void:
70-
# Update the peak meter in real-time
71-
var peak_db = recorder.get_peak_volume_db().x
72-
_update_volume_meter(peak_db, delta)
73-
74-
# --- UI Logic ---
75-
76-
func _update_volume_meter(peak_db: float, delta: float) -> void:
77-
var energy = db_to_linear(peak_db)
78-
var current_val = volume_progress_bar.value
79+
func _update_volume_meter_logic(delta: float) -> void:
80+
var peak_db: float = recorder.get_peak_volume_db().x
81+
var energy := db_to_linear(peak_db)
82+
var current_val := volume_progress_bar.value
7983

80-
# Instant rise, smooth fall (classic peak meter behavior)
81-
#if energy > current_val:
82-
#volume_progress_bar.value = energy
83-
#else:
84+
# Smooth fall-off for the meter visualization
8485
volume_progress_bar.value = lerp(current_val, energy, meter_smooth_speed * delta)
85-
86+
8687
# --- Signal Callbacks: Recorder ---
8788

8889
func _on_start_and_stop_button_pressed() -> void:
@@ -94,19 +95,23 @@ func _on_start_and_stop_button_pressed() -> void:
9495
func _on_recorder_on_recording_start() -> void:
9596
start_and_stop_button.text = "Stop Recording"
9697
start_and_stop_button.modulate = Color.RED
97-
play_stop_button.disabled = true
98-
save_button.disabled = true
98+
_toggle_playback_controls(true)
9999

100100
func _on_recorder_on_recording_end() -> void:
101101
start_and_stop_button.text = "Record"
102102
start_and_stop_button.modulate = Color.WHITE
103103

104-
# Generate the 24-bit resource immediately after recording
104+
# Cache recorded resources
105105
_last_recorded_resource24b = recorder.get_recording_as_wav24b()
106106
_last_recorded_resource = recorder.get_recording()
107107

108-
play_stop_button.disabled = false
109-
save_button.disabled = false
108+
_toggle_playback_controls(false)
109+
110+
func _toggle_playback_controls(is_recording: bool) -> void:
111+
play_stop_button.disabled = is_recording
112+
save_button.disabled = is_recording
113+
114+
# --- Signal Callbacks: Playback ---
110115

111116
# --- Signal Callbacks: Playback ---
112117

@@ -117,7 +122,7 @@ func _on_play_stop_button_pressed() -> void:
117122
_stop_playback()
118123

119124
func _start_playback() -> void:
120-
if _last_recorded_resource:
125+
if _last_recorded_resource24b:
121126
player_24bit.play_24bit(_last_recorded_resource24b)
122127
play_stop_button.text = "Stop Playback"
123128
_is_playing = true
@@ -130,57 +135,62 @@ func _stop_playback() -> void:
130135
func _on_player_24bit_finished() -> void:
131136
_stop_playback()
132137

133-
# --- Signal Callbacks: File Management ---
138+
# --- File Management & Android Workaround ---
134139

135140
func _on_save_button_pressed() -> void:
136141
if not _last_recorded_resource:
142+
push_warning("No recording available to save.")
137143
return
138144

139-
var file_name = "recording_%s.wav" % _generate_uuid().substr(0, 8)
145+
var file_name := "rec_%s.wav" % _generate_short_id()
146+
var temp_path := "user://" + file_name
147+
var save_error: Error
140148

141-
var file_path = "user://" + file_name
149+
# 1. High-speed internal save (Direct IO)
150+
if recorder.format == 2: # 24-bit format index
151+
save_error = _last_recorded_resource24b.save_to_wav(temp_path)
152+
else:
153+
save_error = _last_recorded_resource.save_to_wav(temp_path)
142154

155+
if save_error != OK:
156+
push_error("Failed to save temporary file. Error: ", save_error)
157+
return
158+
159+
# 2. Android Scoped Storage Workaround
143160
if OS.get_name() == "Android":
144-
file_path = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) + "/recordings/" + file_name
161+
_export_to_android_documents(temp_path, file_name)
162+
else:
163+
print("File saved to user data: ", ProjectSettings.globalize_path(temp_path))
164+
165+
# Moves a file from internal storage to the public Documents folder on Android.
166+
func _export_to_android_documents(source_path: String, file_name: String) -> void:
167+
var docs_dir := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS).path_join(ANDROID_RECORDINGS_SUBDIR)
145168

146-
var error
169+
# Ensure directory exists
170+
if not DirAccess.dir_exists_absolute(docs_dir):
171+
DirAccess.make_dir_recursive_absolute(docs_dir)
147172

148-
if (recorder.format == 2):
149-
error = _last_recorded_resource24b.save_to_wav(file_path)
150-
else:
151-
error = _last_recorded_resource.save_to_wav(file_path)
173+
var destination_path := docs_dir.path_join(file_name)
174+
var dir := DirAccess.open("user://")
152175

153-
if error == OK:
154-
print("Success: %s audio saved at: " % recorder.available_formats()[recorder.format], ProjectSettings.globalize_path(file_path))
176+
if dir and dir.copy(source_path, destination_path) == OK:
177+
print("Successfully exported to Documents: ", destination_path)
178+
dir.remove(source_path) # Clean up temp file
155179
else:
156-
push_error("Failed to save audio. Error code: ", error)
180+
push_error("Android Export Failed. Path: ", destination_path)
157181

158182
func _on_option_button_item_selected(index: int) -> void:
159183
recorder.format = index
160184

161185
# --- Helpers ---
162186

163-
## Generates a unique identifier for file naming.
164-
func _generate_uuid() -> String:
165-
var crypto = Crypto.new()
166-
var b = crypto.generate_random_bytes(16)
167-
168-
# UUID v4 specific bit manipulation
169-
b[6] = (b[6] & 0x0f) | 0x40
170-
b[8] = (b[8] & 0x3f) | 0x80
171-
172-
var hex = b.hex_encode()
173-
return "%s-%s-%s-%s-%s" % [
174-
hex.substr(0, 8), hex.substr(8, 4),
175-
hex.substr(12, 4), hex.substr(16, 4),
176-
hex.substr(20, 12)
177-
]
187+
## Generates a short random ID using crypto-safe bytes.
188+
func _generate_short_id() -> String:
189+
var crypto := Crypto.new()
190+
return crypto.generate_random_bytes(4).hex_encode()
178191

179192
func _on_input_devices_option_item_selected(index: int) -> void:
180-
var s = input_devices_option.get_item_text(index)
181-
AudioServer.input_device = s
182-
193+
AudioServer.input_device = input_devices_option.get_item_text(index)
183194

184195
func _on_output_devices_option_item_selected(index: int) -> void:
185-
var e = output_devices_option.get_item_text(index)
186-
AudioServer.output_device = e
196+
AudioServer.output_device = output_devices_option.get_item_text(index)

addons/direct_audio_and_wav_24bits/demo/demo_scene.tscn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,4 @@ text_overrun_behavior = 4
211211
[connection signal="pressed" from="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/PanelContainer/MarginContainer/VBoxContainer/SaveButton" to="." method="_on_save_button_pressed"]
212212
[connection signal="item_selected" from="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/OptionButton" to="." method="_on_option_button_item_selected"]
213213
[connection signal="item_selected" from="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer3/HBoxContainer/InputDevicesOption" to="." method="_on_input_devices_option_item_selected"]
214+
[connection signal="item_selected" from="CenterContainer/PanelContainer/MarginContainer/VBoxContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer3/HBoxContainer2/OutputDevicesOption" to="." method="_on_output_devices_option_item_selected"]

addons/direct_audio_and_wav_24bits/scripts/audio_stream_player_wav_24b.gd

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var stream_resource: AudioStreamWAV24B
1515
var _playback: AudioStreamGeneratorPlayback
1616
var _current_frame_index: int = 0
1717
var _is_initialized: bool = false
18+
var _was_pushed_completely: bool = false
1819

1920
# --- Built-in Node Methods ---
2021

@@ -35,6 +36,7 @@ func play_24bit(res: AudioStreamWAV24B) -> void:
3536

3637
stream_resource = res
3738
_current_frame_index = 0
39+
_was_pushed_completely = false # Resetear estado
3840

3941
# Ensure the generator's mix rate matches the resource
4042
if stream.mix_rate != res.mix_rate:
@@ -49,12 +51,14 @@ func play_24bit(res: AudioStreamWAV24B) -> void:
4951
## Resets the playback cursor to the beginning.
5052
func seek_start() -> void:
5153
_current_frame_index = 0
54+
_was_pushed_completely = false
5255

5356
# --- Internal Methods ---
5457

5558
## Initializes the AudioStreamGenerator which acts as a bridge for the raw data.
5659
func _setup_generator() -> void:
5760
var generator = AudioStreamGenerator.new()
61+
5862
generator.mix_rate = 44100 # Default, will be updated on play
5963
generator.buffer_length = 0.1 # 100ms buffer for low latency
6064

@@ -73,14 +77,30 @@ func _fill_buffer() -> void:
7377

7478
# Calculate how many frames are left in the actual audio data
7579
var total_resource_frames = stream_resource.data.size() / (6 if stream_resource.stereo else 3)
80+
81+
# 1. ¿Ya enviamos todo al buffer?
82+
if _current_frame_index >= total_resource_frames:
83+
_was_pushed_completely = true
84+
85+
# 2. Esperar a que el buffer del AudioServer se vacíe
86+
# Si los frames disponibles son iguales al tamaño total del buffer, es que ya no hay nada sonando
87+
var buffer_capacity = stream.buffer_length * stream.mix_rate
88+
if frames_available >= buffer_capacity - 1: # -1 por margen de error de redondeo
89+
_finalize_playback()
90+
return
91+
92+
if frames_available <= 0:
93+
return
94+
7695
var frames_to_process = min(frames_available, total_resource_frames - _current_frame_index)
7796

7897
if frames_to_process <= 0:
7998
if _current_frame_index >= total_resource_frames:
8099
# End of file reached
81100
stop()
82101
return
83-
102+
103+
# --- DECODING LOOP ---
84104
# Create a temporary buffer for the chunk
85105
var chunk = PackedVector2Array()
86106
chunk.resize(frames_to_process)
@@ -112,3 +132,9 @@ func _fill_buffer() -> void:
112132
# Push the decoded chunk to the AudioServer
113133
_playback.push_buffer(chunk)
114134
_current_frame_index += frames_to_process
135+
136+
func _finalize_playback() -> void:
137+
stop()
138+
_current_frame_index = 0
139+
_was_pushed_completely = false
140+
finished.emit()

assets/.gdignore

Whitespace-only changes.

assets/banner.webp.import

Lines changed: 0 additions & 3 deletions
This file was deleted.

assets/icon.png.import

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)