diff --git a/audio/mic_feed/Intro.ogg b/audio/mic_feed/Intro.ogg new file mode 100644 index 00000000000..5d65f045e10 Binary files /dev/null and b/audio/mic_feed/Intro.ogg differ diff --git a/audio/mic_feed/Intro.ogg.import b/audio/mic_feed/Intro.ogg.import new file mode 100644 index 00000000000..0b738f73fca --- /dev/null +++ b/audio/mic_feed/Intro.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://c2re52petqrvx" +path="res://.godot/imported/Intro.ogg-dfe75727d0e47692e220adf97ddb7ad9.oggvorbisstr" + +[deps] + +source_file="res://Intro.ogg" +dest_files=["res://.godot/imported/Intro.ogg-dfe75727d0e47692e220adf97ddb7ad9.oggvorbisstr"] + +[params] + +loop=true +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/audio/mic_feed/MicRecord.gd b/audio/mic_feed/MicRecord.gd new file mode 100644 index 00000000000..a6b1ce3cf59 --- /dev/null +++ b/audio/mic_feed/MicRecord.gd @@ -0,0 +1,189 @@ +extends Control + +var wav_recording: AudioStreamWAV +var input_mix_rate: int = 44100 +var audio_chunk_size_ms: int = 20 +var audio_sample_size: int = 882 + +var total_samples: int = 0 +var sample_duration: float = 0.0 +var recording_buffer: Variant = null + +var audio_sample_image: Image +var audio_sample_texture: ImageTexture +var generator_timestamp: float = 0.0 +var generator_freq: float = 0.0 + +var microphone_feed = null + + +func _ready() -> void: + for d in AudioServer.get_input_device_list(): + $OptionInput.add_item(d) + assert($OptionInput.get_item_text($OptionInput.selected) == "Default") + + for d in AudioServer.get_output_device_list(): + $OptionOutput.add_item(d) + assert($OptionOutput.get_item_text($OptionOutput.selected) == "Default") + + input_mix_rate = int(AudioServer.get_input_mix_rate()) + print("Input mix rate: ", input_mix_rate) + print("Output mix rate: ", AudioServer.get_mix_rate()) + print("Project mix rate: ", ProjectSettings.get(&"audio/driver/mix_rate")) + + if Engine.has_singleton("MicrophoneServer"): + microphone_feed = Engine.get_singleton("MicrophoneServer").get_feed(0) + if not microphone_feed: + $Status.text = "**** Error: requires PR#108773 to work" + print($Status.text) + set_process(false) + $MicrophoneOn.disabled = true + + $InputMixRate.text = "Mix rate: %d" % input_mix_rate + audio_sample_size = int(audio_chunk_size_ms * input_mix_rate / 1000.0) + + var blank_image: PackedVector2Array = PackedVector2Array() + blank_image.resize(audio_sample_size) + audio_sample_image = Image.create_from_data(audio_sample_size, 1, false, Image.FORMAT_RGF, blank_image.to_byte_array()) + audio_sample_texture = ImageTexture.create_from_image(audio_sample_image) + $MicTexture.material.set_shader_parameter(&"audiosample", audio_sample_texture) + + +func _on_option_input_item_selected(index: int) -> void: + var input_device: String = $OptionInput.get_item_text(index) + print("Set input device: ", input_device) + AudioServer.set_input_device(input_device) + + +func _on_option_output_item_selected(index: int) -> void: + var output_device: String = $OptionOutput.get_item_text(index) + print("Set output device: ", output_device) + AudioServer.set_output_device(output_device) + + +func _on_microphone_on_toggled(toggled_on: bool) -> void: + if toggled_on: + if OS.get_name() == "Android" and not OS.request_permission("android.permission.RECORD_AUDIO"): + print("Waiting for user response after requesting audio permissions") + # yuou also need to enabled Record Audio in the android export settings + @warning_ignore("untyped_declaration") + var x = await get_tree().on_request_permissions_result + var permission: String = x[0] + var granted: bool = x[1] + assert(permission == "android.permission.RECORD_AUDIO") + print("Audio permission granted ", granted) + + if not microphone_feed.is_active(): + microphone_feed.set_active(true) + total_samples = 0 + sample_duration = 0.0 + print("Input buffer length frames: ", microphone_feed.get_buffer_length_frames()) + print("Input buffer length seconds: ", microphone_feed.get_buffer_length_frames() * 1.0 / input_mix_rate) + else: + microphone_feed.set_active(false) + + +func _on_mic_to_generator_toggled(toggled_on: bool) -> void: + if toggled_on: + $AudioGenerator.stream.mix_rate = input_mix_rate + $AudioGenerator.playing = toggled_on + + +func _process(delta: float) -> void: + sample_duration += delta + while microphone_feed.get_frames_available() >= audio_sample_size: + var audio_samples: PackedVector2Array = microphone_feed.get_frames(audio_sample_size) + if audio_samples: + audio_sample_image.set_data(audio_sample_size, 1, false, Image.FORMAT_RGF, audio_samples.to_byte_array()) + audio_sample_texture.update(audio_sample_image) + total_samples += 1 + $SampleCount.text = "%.0f samples/sec" % (total_samples * audio_sample_size / sample_duration) + if recording_buffer != null: + recording_buffer.append(audio_samples) + if $MicToGenerator.button_pressed: + $AudioGenerator.get_stream_playback().push_buffer(audio_samples) + if generator_freq != 0.0: + var gplayback: AudioStreamGeneratorPlayback = $AudioGenerator.get_stream_playback() + var gdt: float = 1.0 / $AudioGenerator.stream.mix_rate + for i in range(gplayback.get_frames_available()): + var a: float = 0.5 * sin(generator_timestamp * generator_freq * TAU) + gplayback.push_frame(Vector2(a, a)) + generator_timestamp += gdt + + +func _on_record_button_toggled(toggled_on: bool) -> void: + total_samples = 0 + sample_duration = 0.0 + if toggled_on: + $PlayButton.disabled = true + $SaveButton.disabled = true + recording_buffer = [] + $RecordButton.text = "Stop" + $Status.text = "Status: Recording..." + + else: + $PlayButton.disabled = false + $SaveButton.disabled = false + var recording_data: PackedByteArray = PackedByteArray() + var data_size: int = 4 * audio_sample_size * len(recording_buffer) + recording_data.resize(44 + data_size) + recording_data.encode_u32(0, 0x46464952) # RIFF + recording_data.encode_u32(4, len(recording_data) - 8) + recording_data.encode_u32(8, 0x45564157) # WAVE + recording_data.encode_u32(12, 0x20746D66) # 'fmt ' + recording_data.encode_u32(16, 16) + recording_data.encode_u16(20, 1) + recording_data.encode_u16(22, 2) + recording_data.encode_u32(24, input_mix_rate) + recording_data.encode_u32(28, input_mix_rate * 4) # *16*2/8 + recording_data.encode_u16(32, 4) # 16*2/8 + recording_data.encode_u16(34, 16) + recording_data.encode_u32(36, 0x61746164) # 'data' + recording_data.encode_u32(40, data_size) + for i in range(len(recording_buffer)): + for j in range(audio_sample_size): + var k: int = 44 + 4 * (i * audio_sample_size + j) + recording_data.encode_s16(k, clampi(recording_buffer[i][j].x * 32768, -32768, 32767)) + recording_data.encode_s16(k + 2, clampi(recording_buffer[i][j].y * 32768, -32768, 32767)) + wav_recording = AudioStreamWAV.load_from_buffer(recording_data) + + $RecordButton.text = "Record" + $Status.text = "" + recording_buffer = null + + +func _on_play_button_pressed() -> void: + print_rich("\n[b]Playing recording:[/b] %s" % wav_recording) + $AudioWav.stream = wav_recording + $AudioWav.play() + + +func _on_play_music_toggled(toggled_on: bool) -> void: + if toggled_on: + $AudioMusic.play() + $PlayMusic.text = "Stop Music" + else: + $AudioMusic.stop() + $PlayMusic.text = "Play Music" + + +func _on_save_button_pressed() -> void: + var save_path: String = $SaveButton/Filename.text + wav_recording.save_to_wav(save_path) + $Status.text = "Status: Saved WAV file to: %s\n(%s)" % [save_path, ProjectSettings.globalize_path(save_path)] + + +func _on_open_user_folder_button_pressed() -> void: + OS.shell_open(ProjectSettings.globalize_path("user://")) + + +# 400Hz frequency can be used (from another device) to probe a stereo microphone +# response due to where there should be 8 wavelengths in the space of 20ms (2.5ms per wave). +# The wavelength is then 343/400=0.8575m long. +func _on_option_tone_item_selected(index: int) -> void: + if index != 0: + $AudioGenerator.playing = true + if not $MicToGenerator.button_pressed and not $PlayMusic.button_pressed: + generator_freq = int($OptionTone.get_item_text(index)) + else: + generator_freq = 0.0 diff --git a/audio/mic_feed/MicRecord.gd.uid b/audio/mic_feed/MicRecord.gd.uid new file mode 100644 index 00000000000..2512b3508a7 --- /dev/null +++ b/audio/mic_feed/MicRecord.gd.uid @@ -0,0 +1 @@ +uid://dbbfvbf6ronrp diff --git a/audio/mic_feed/MicRecord.gdshader b/audio/mic_feed/MicRecord.gdshader new file mode 100644 index 00000000000..0b7b48732e1 --- /dev/null +++ b/audio/mic_feed/MicRecord.gdshader @@ -0,0 +1,26 @@ +shader_type canvas_item; +render_mode blend_mix; + +uniform sampler2D audiosample : repeat_enable; +const float cfac = 4.0; +const float mfac = 2.0; +const float mdisp = 0.166667; +const float mthick = 0.05; +const float mtiltfac = 0.125; + +void fragment() { + vec4 b = texture(audiosample, UV + vec2(-(UV.y - 0.5) * mtiltfac, 0.0)); + vec4 c = texture(audiosample, UV + vec2((UV.y - 0.5) * mtiltfac, 0.0)); + float s = (b.r + c.g) / 2.0; + COLOR = vec4(0.1 + max(s, 0.0) * cfac, 0.1, 0.1 + max(-s, 0.0) * cfac, 1.0); + + vec4 a = texture(audiosample, UV); + float dr = abs(UV.y * 2.0 - 1.0 - (a.r + mdisp) * mfac); + float dg = abs(UV.y * 2.0 - 1.0 - (a.g - mdisp) * mfac); + + if (dg < mthick) { + COLOR = vec4(1.0, 1.0, 0.9, 1.0); + } else if (dr < mthick) { + COLOR = vec4(0.8, 0.8, 0.9, 1.0); + } +} diff --git a/audio/mic_feed/MicRecord.gdshader.uid b/audio/mic_feed/MicRecord.gdshader.uid new file mode 100644 index 00000000000..8bb191a586c --- /dev/null +++ b/audio/mic_feed/MicRecord.gdshader.uid @@ -0,0 +1 @@ +uid://cl4x5tyii4r6q diff --git a/audio/mic_feed/MicRecord.tscn b/audio/mic_feed/MicRecord.tscn new file mode 100644 index 00000000000..78a8562bb8f --- /dev/null +++ b/audio/mic_feed/MicRecord.tscn @@ -0,0 +1,213 @@ +[gd_scene load_steps=7 format=3 uid="uid://dvjlkpjvjxn0h"] + +[ext_resource type="Script" uid="uid://dbbfvbf6ronrp" path="res://MicRecord.gd" id="1"] +[ext_resource type="AudioStream" uid="uid://c2re52petqrvx" path="res://Intro.ogg" id="2"] +[ext_resource type="Shader" uid="uid://cl4x5tyii4r6q" path="res://MicRecord.gdshader" id="3_vkk4n"] + +[sub_resource type="AudioStreamMicrophone" id="AudioStreamMicrophone_tfvr1"] + +[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_tfvr1"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_m4htd"] +shader = ExtResource("3_vkk4n") + +[node name="MicRecord" type="Control"] +layout_mode = 3 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -320.0 +offset_top = -240.0 +offset_right = 254.0 +offset_bottom = 210.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] +stream = SubResource("AudioStreamMicrophone_tfvr1") +bus = &"Microphone" + +[node name="AudioWav" type="AudioStreamPlayer" parent="."] +autoplay = true + +[node name="AudioMusic" type="AudioStreamPlayer" parent="."] +stream = ExtResource("2") +volume_db = -6.0 + +[node name="AudioGenerator" type="AudioStreamPlayer" parent="."] +stream = SubResource("AudioStreamGenerator_tfvr1") +bus = &"Generate" + +[node name="Status" type="Label" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.0 +offset_bottom = 26.0 +grow_horizontal = 2 +text = "Status: " +horizontal_alignment = 1 + +[node name="MicrophoneOn" type="CheckBox" parent="."] +layout_mode = 0 +offset_left = 5.0 +offset_top = 50.0 +offset_right = 154.0 +offset_bottom = 81.0 +tooltip_text = "Nothing works until you +turn the microphone on." +text = "Microphone On +" + +[node name="MicToGenerator" type="CheckBox" parent="."] +layout_mode = 0 +offset_left = 156.0 +offset_top = 50.0 +offset_right = 315.0 +offset_bottom = 81.0 +tooltip_text = "Warning: this will cause feedback +unless you use headphones" +text = "Mic to Output +" + +[node name="InputMixRate" type="Label" parent="."] +layout_mode = 0 +offset_left = 323.0 +offset_top = 56.0 +offset_right = 392.0 +offset_bottom = 79.0 +text = "Mix rate:" + +[node name="SampleCount" type="Label" parent="."] +layout_mode = 0 +offset_left = 460.0 +offset_top = 56.0 +offset_right = 542.0 +offset_bottom = 79.0 +text = "N-samples" + +[node name="RecordButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 25.0 +offset_top = 167.0 +offset_right = 155.0 +offset_bottom = 207.0 +toggle_mode = true +text = "Record" + +[node name="PlayButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 172.0 +offset_top = 167.0 +offset_right = 302.0 +offset_bottom = 207.0 +disabled = true +text = "Play" + +[node name="PlayMusic" type="Button" parent="."] +layout_mode = 0 +offset_left = 328.0 +offset_top = 167.0 +offset_right = 458.0 +offset_bottom = 207.0 +toggle_mode = true +text = "Play Music" + +[node name="MicTexture" type="ColorRect" parent="."] +material = SubResource("ShaderMaterial_m4htd") +layout_mode = 0 +offset_left = 25.0 +offset_top = 223.0 +offset_right = 553.0 +offset_bottom = 312.0 + +[node name="SaveButton" type="Button" parent="."] +layout_mode = 0 +offset_left = 31.0 +offset_top = 357.0 +offset_right = 161.0 +offset_bottom = 397.0 +disabled = true +toggle_mode = true +text = "Save WAV To:" + +[node name="Filename" type="LineEdit" parent="SaveButton"] +layout_mode = 0 +offset_left = 150.0 +offset_right = 477.0 +offset_bottom = 40.0 +text = "user://record.wav" +caret_blink = true + +[node name="OpenUserFolderButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = -1 +offset_left = 33.0 +offset_top = 411.0 +offset_right = 196.0 +offset_bottom = 451.0 +text = "Open User Folder" + +[node name="OptionTone" type="OptionButton" parent="."] +layout_mode = 0 +offset_left = 479.0 +offset_top = 171.0 +offset_right = 573.0 +offset_bottom = 202.0 +selected = 0 +item_count = 5 +popup/item_0/text = "No tone" +popup/item_0/id = 0 +popup/item_1/text = "200Hz" +popup/item_1/id = 1 +popup/item_2/text = "400Hz" +popup/item_2/id = 2 +popup/item_3/text = "1000Hz" +popup/item_3/id = 3 +popup/item_4/text = "2000Hz" +popup/item_4/id = 4 + +[node name="LabelInputDevice" type="Label" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_bottom = 0.5 +offset_left = 16.0 +offset_top = 85.0 +offset_right = 135.392 +offset_bottom = -112.714 +text = "Input device:" + +[node name="OptionInput" type="OptionButton" parent="."] +layout_mode = 0 +offset_left = 137.0 +offset_top = 88.0 +offset_right = 169.0 +offset_bottom = 108.0 + +[node name="LabelOutputDevice" type="Label" parent="."] +layout_mode = 0 +offset_left = 17.0 +offset_top = 120.0 +offset_right = 57.0 +offset_bottom = 143.0 +text = "Output device:" + +[node name="OptionOutput" type="OptionButton" parent="."] +layout_mode = 0 +offset_left = 144.0 +offset_top = 122.0 +offset_right = 176.0 +offset_bottom = 142.0 + +[connection signal="toggled" from="MicrophoneOn" to="." method="_on_microphone_on_toggled"] +[connection signal="toggled" from="MicToGenerator" to="." method="_on_mic_to_generator_toggled"] +[connection signal="toggled" from="RecordButton" to="." method="_on_record_button_toggled"] +[connection signal="pressed" from="PlayButton" to="." method="_on_play_button_pressed"] +[connection signal="toggled" from="PlayMusic" to="." method="_on_play_music_toggled"] +[connection signal="pressed" from="SaveButton" to="." method="_on_save_button_pressed"] +[connection signal="pressed" from="OpenUserFolderButton" to="." method="_on_open_user_folder_button_pressed"] +[connection signal="item_selected" from="OptionTone" to="." method="_on_option_tone_item_selected"] +[connection signal="item_selected" from="OptionInput" to="." method="_on_option_input_item_selected"] +[connection signal="item_selected" from="OptionOutput" to="." method="_on_option_output_item_selected"] diff --git a/audio/mic_feed/README.md b/audio/mic_feed/README.md new file mode 100644 index 00000000000..7c4af4c2418 --- /dev/null +++ b/audio/mic_feed/README.md @@ -0,0 +1,19 @@ +# Audio Mic Feed + +This example shows how to read microphone audio input data +using the `PackedVector2Array Input.get_microphone_buffer(frames: int)` +function. + +The data can be copied to an `AudioStreamGenerator`, saved to a WAV file, or +used as a `FORMAT_RGF` image by a GPU shader. + +A sine wave tone generator is included that can be deployed on a second device +and be used to probe the positional effects on a stereo microphone. + +Language: GDScript + +Renderer: Compatibility + +## Screenshots + +![image](https://github.com/user-attachments/assets/d85360dd-a0aa-4694-aad0-d570fd2a6a15) diff --git a/audio/mic_feed/default_bus_layout.tres b/audio/mic_feed/default_bus_layout.tres new file mode 100644 index 00000000000..f9a9c7e6229 --- /dev/null +++ b/audio/mic_feed/default_bus_layout.tres @@ -0,0 +1,20 @@ +[gd_resource type="AudioBusLayout" load_steps=2 format=3 uid="uid://tuxl6tvrr2dv"] + +[sub_resource type="AudioEffectReverb" id="AudioEffectReverb_j3pel"] +resource_name = "Reverb" + +[resource] +bus/1/name = &"Generate" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" +bus/1/effect/0/effect = SubResource("AudioEffectReverb_j3pel") +bus/1/effect/0/enabled = false +bus/2/name = &"Microphone" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = -0.52823544 +bus/2/send = &"Master" diff --git a/audio/mic_feed/icon.webp b/audio/mic_feed/icon.webp new file mode 100644 index 00000000000..166cc566489 Binary files /dev/null and b/audio/mic_feed/icon.webp differ diff --git a/audio/mic_feed/icon.webp.import b/audio/mic_feed/icon.webp.import new file mode 100644 index 00000000000..cc29159c5b4 --- /dev/null +++ b/audio/mic_feed/icon.webp.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://brwp8bimc75uu" +path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.webp" +dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/audio/mic_feed/project.godot b/audio/mic_feed/project.godot new file mode 100644 index 00000000000..7fbac1d3d96 --- /dev/null +++ b/audio/mic_feed/project.godot @@ -0,0 +1,43 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Audio Mic Feed Demo" +config/description="This is an example showing how one can record audio from +the microphone and later play it back or save it to a file." +config/tags=PackedStringArray("audio", "demo", "official") +run/main_scene="res://MicRecord.tscn" +config/features=PackedStringArray("4.6") +run/low_processor_mode=true +config/icon="res://icon.webp" + +[audio] + +driver/enable_input=true +enable_audio_input=true + +[debug] + +gdscript/warnings/untyped_declaration=1 + +[display] + +window/size/viewport_width=640 +window/size/viewport_height=480 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" +window/vsync/vsync_mode=0 + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +textures/vram_compression/import_etc2_astc=true diff --git a/audio/mic_feed/screenshots/.gdignore b/audio/mic_feed/screenshots/.gdignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/audio/mic_feed/screenshots/mic_record.png b/audio/mic_feed/screenshots/mic_record.png new file mode 100644 index 00000000000..57ded2fd3d7 Binary files /dev/null and b/audio/mic_feed/screenshots/mic_record.png differ