mirror of
https://github.com/deunix-educ/aventuriers-des-mots.git
synced 2026-10-13 02:11:58 +02:00
Add choice between five background music tracks (0.5.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://bx25q1y3gawje"
|
||||
path="res://.godot/imported/meadow.ogg-c923775439afb03b78187075ddba7157.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/music/meadow.ogg"
|
||||
dest_files=["res://.godot/imported/meadow.ogg-c923775439afb03b78187075ddba7157.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://bn0lqeepcj6v6"
|
||||
path="res://.godot/imported/night.ogg-662b927d91ff65e14ea877eeff9e3458.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/music/night.ogg"
|
||||
dest_files=["res://.godot/imported/night.ogg-662b927d91ff65e14ea877eeff9e3458.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://4usotc2day54"
|
||||
path="res://.godot/imported/river.ogg-39ddba8950bdb36622cac8424794d138.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/music/river.ogg"
|
||||
dest_files=["res://.godot/imported/river.ogg-39ddba8950bdb36622cac8424794d138.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -76,6 +76,12 @@ func play_music(music_id: String) -> void:
|
||||
_music_player.play()
|
||||
|
||||
|
||||
## Musique de fond selon le choix des reglages et le niveau scolaire de la
|
||||
## scene appelante (« automatique » = celle de la region).
|
||||
func play_region_music(level: String) -> void:
|
||||
play_music(SoundCatalog.resolve_music(SettingsManager.music_track, level))
|
||||
|
||||
|
||||
func current_music_id() -> String:
|
||||
return _current_music_id
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ var animations_enabled := true
|
||||
var volume := 0.8
|
||||
var music_volume := 0.5
|
||||
var effects_volume := 0.8
|
||||
var music_track := SoundCatalog.MUSIC_AUTO
|
||||
var text_scale_index := 0
|
||||
|
||||
var _base_font_sizes: Dictionary = {}
|
||||
@@ -49,6 +50,11 @@ func set_effects_volume(value: float) -> void:
|
||||
_commit()
|
||||
|
||||
|
||||
func set_music_track(choice: String) -> void:
|
||||
music_track = choice if SoundCatalog.music_choices().has(choice) else SoundCatalog.MUSIC_AUTO
|
||||
_commit()
|
||||
|
||||
|
||||
func set_text_scale_index(index: int) -> void:
|
||||
text_scale_index = clampi(index, 0, AppSettings.TEXT_SCALES.size() - 1)
|
||||
_commit()
|
||||
@@ -69,6 +75,7 @@ func load_from_disk() -> void:
|
||||
volume = clean["volume"]
|
||||
music_volume = clean["music_volume"]
|
||||
effects_volume = clean["effects_volume"]
|
||||
music_track = clean["music_track"]
|
||||
text_scale_index = clean["text_scale_index"]
|
||||
animations_enabled = clean["animations_enabled"]
|
||||
|
||||
@@ -79,6 +86,7 @@ func save_to_disk() -> bool:
|
||||
data["volume"] = volume
|
||||
data["music_volume"] = music_volume
|
||||
data["effects_volume"] = effects_volume
|
||||
data["music_track"] = music_track
|
||||
data["text_scale_index"] = text_scale_index
|
||||
data["animations_enabled"] = animations_enabled
|
||||
var tmp_path := SETTINGS_PATH + ".tmp"
|
||||
|
||||
@@ -109,5 +109,12 @@
|
||||
"settings.animations_on": "Avec animations",
|
||||
"settings.animations_off": "Sans animations",
|
||||
"settings.music_label": "Musique",
|
||||
"settings.effects_label": "Effets sonores"
|
||||
"settings.effects_label": "Effets sonores",
|
||||
"settings.track_label": "Musique de fond",
|
||||
"music.auto": "Automatique (selon le lieu)",
|
||||
"music.forest": "Forêt",
|
||||
"music.marsh": "Marais",
|
||||
"music.meadow": "Prairie",
|
||||
"music.river": "Rivière",
|
||||
"music.night": "Nuit"
|
||||
}
|
||||
|
||||
@@ -326,3 +326,5 @@ Ajouts à l'arborescence proposée :
|
||||
**Voix plus naturelles** : non traité. La voix Piper `fr_FR-siwis-medium` reste la voix provisoire (point 19) ; une voix plus naturelle suppose un enregistrement humain ou un autre modèle à évaluer à l'oreille, ce que je ne peux pas faire ici. Les dictées gardent le badge « voix provisoire ».
|
||||
|
||||
**Tests** : catalogue (fichiers présents, aucun chemin libre), `AudioManager` (lecture, musique mémorisée), bus idempotents, bornes des volumes. **Non vérifié** : tout ce qui s'entend (timbre, niveau relatif musique/effets/voix, boucle sans claquement, agrément) ; vérifié seulement : durées et niveaux de crête des fichiers (aucun écrêtage).
|
||||
|
||||
**Addendum (0.5.1) — choix de la musique de fond** (demande utilisateur) : 5 morceaux au choix, `forest`, `marsh`, `meadow` (prairie), `river` (rivière), `night` (nuit), plus un choix « Automatique » (par défaut : forêt en CE1, marais en CE2, comme avant). Le choix est un réglage de l'appareil (`music_track` dans `user://settings.json`, valeur inconnue ramenée à « auto ») : le format des sauvegardes de profil est inchangé. Sélectionner un morceau dans les réglages le fait entendre aussitôt. Sons toujours provisoires et jamais écoutés ici.
|
||||
|
||||
@@ -43,7 +43,7 @@ Tout ce qui suit ne peut pas être vérifié sans écran (rendu, mise en page, a
|
||||
|
||||
## Audio
|
||||
|
||||
- Son et musique (point 33 de `decisions.md`) : clic sur les boutons, « Bravo » (accord montant) et « Pas tout à fait » (deux notes douces, jamais agressives), étoiles une à une puis fanfare à 3 étoiles ou objet gagné ; musique de forêt (CE1) et de marais (CE2), en boucle sans coupure audible, qui baisse pendant une dictée ; trois curseurs de réglages qui agissent bien chacun sur leur bus ; tout reste agréable à volume moyen et il n'y a jamais d'écrasement de la voix.
|
||||
- Son et musique (point 33 de `decisions.md`) : clic sur les boutons, « Bravo » (accord montant) et « Pas tout à fait » (deux notes douces, jamais agressives), étoiles une à une puis fanfare à 3 étoiles ou objet gagné ; musique de forêt (CE1) et de marais (CE2), en boucle sans coupure audible, qui baisse pendant une dictée ; cinq morceaux au choix dans « Musique de fond » (le choix s'entend aussitôt et est conservé après redémarrage) ; trois curseurs de réglages qui agissent bien chacun sur leur bus ; tout reste agréable à volume moyen et il n'y a jamais d'écrasement de la voix.
|
||||
|
||||
- `ce1_accord_adjectif_dictee_01` a un fichier audio provisoire généré par `make generate-dictation-audio` (voix neuronale Piper, voir `assets/CREDITS.md`). Testé manuellement : voix claire et bien comprise.
|
||||
- Toute dictée ajoutée sans régénérer son audio se dégrade silencieusement (avertissement en console, pas de plantage) : vérifier qu'aucune dictée manquante ne passe inaperçue avant diffusion (`make generate-dictation-audio` ne régénère pas l'existant, `--force` pour forcer).
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="Les Aventuriers des mots"
|
||||
config/version="0.5.0"
|
||||
config/version="0.5.1"
|
||||
run/main_scene="res://scenes/boot/boot.tscn"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
@@ -66,7 +66,7 @@ func _ready() -> void:
|
||||
_skill_id = _rule.get("skill", "")
|
||||
_current_difficulty = ProgressManager.next_difficulty_for_skill(_skill_id, 1)
|
||||
$Backdrop.kind = "marsh" if _rule.get("level", "CE1") == "CE2" else "forest"
|
||||
AudioManager.play_music(SoundCatalog.music_for_level(_rule.get("level", "CE1")))
|
||||
AudioManager.play_region_music(_rule.get("level", "CE1"))
|
||||
_show_rule_panel()
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func _ready() -> void:
|
||||
_new_profile_button.pressed.connect(_on_new_profile_pressed)
|
||||
_delete_confirm_dialog.confirmed.connect(_on_delete_confirmed)
|
||||
|
||||
AudioManager.play_music("forest")
|
||||
AudioManager.play_region_music("CE1")
|
||||
_refresh_profile_list()
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func _ready() -> void:
|
||||
var mission := ExerciseEngine.get_mission(GameManager.pending_mission_id)
|
||||
var rule := ExerciseEngine.get_rule(mission.get("rule_id", ""))
|
||||
_backdrop.kind = "marsh" if rule.get("level", "CE1") == "CE2" else "forest"
|
||||
AudioManager.play_music(SoundCatalog.music_for_level(rule.get("level", "CE1")))
|
||||
AudioManager.play_region_music(rule.get("level", "CE1"))
|
||||
|
||||
var summary := ProgressManager.complete_mission(
|
||||
GameManager.pending_mission_id, GameManager.last_outcomes
|
||||
|
||||
@@ -11,6 +11,8 @@ extends Control
|
||||
@onready var _music_slider: HSlider = %MusicSlider
|
||||
@onready var _effects_label: Label = %EffectsLabel
|
||||
@onready var _effects_slider: HSlider = %EffectsSlider
|
||||
@onready var _track_label: Label = %TrackLabel
|
||||
@onready var _track_option: OptionButton = %TrackOption
|
||||
@onready var _text_size_label: Label = %TextSizeLabel
|
||||
@onready var _text_size_box: HBoxContainer = %TextSizeBox
|
||||
@onready var _animations_label: Label = %AnimationsLabel
|
||||
@@ -36,6 +38,7 @@ func _ready() -> void:
|
||||
_volume_slider.drag_ended.connect(_on_drag_ended)
|
||||
_effects_slider.drag_ended.connect(_on_drag_ended)
|
||||
|
||||
_build_track_option()
|
||||
_build_text_size_buttons()
|
||||
|
||||
_build_animation_buttons()
|
||||
@@ -80,6 +83,21 @@ func _setup_slider(slider: HSlider, value: float, handler: Callable) -> void:
|
||||
slider.value_changed.connect(handler)
|
||||
|
||||
|
||||
func _build_track_option() -> void:
|
||||
_track_label.text = UIText.get_text("settings.track_label")
|
||||
var choices := SoundCatalog.music_choices()
|
||||
for choice in choices:
|
||||
_track_option.add_item(UIText.get_text("music.%s" % choice))
|
||||
_track_option.select(choices.find(SettingsManager.music_track))
|
||||
_track_option.item_selected.connect(_on_track_selected)
|
||||
|
||||
|
||||
func _on_track_selected(index: int) -> void:
|
||||
SettingsManager.set_music_track(SoundCatalog.music_choices()[index])
|
||||
# On fait entendre le choix tout de suite.
|
||||
AudioManager.play_region_music(ProgressManager.school_level())
|
||||
|
||||
|
||||
func _on_volume_changed(value: float) -> void:
|
||||
SettingsManager.set_volume(value)
|
||||
|
||||
|
||||
+17
-1
@@ -41,7 +41,7 @@ custom_minimum_size = Vector2(640, 0)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Margin/Center/Card"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Margin/Center/Card/VBox"]
|
||||
unique_name_in_owner = true
|
||||
@@ -100,6 +100,22 @@ size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
|
||||
[node name="TrackRow" type="HBoxContainer" parent="Margin/Center/Card/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="TrackLabel" type="Label" parent="Margin/Center/Card/VBox/TrackRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(230, 0)
|
||||
theme_type_variation = &"Emphasis"
|
||||
|
||||
[node name="TrackOption" type="OptionButton" parent="Margin/Center/Card/VBox/TrackRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
|
||||
[node name="TextSizeLabel" type="Label" parent="Margin/Center/Card/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
@@ -40,7 +40,7 @@ func _ready() -> void:
|
||||
func _refresh() -> void:
|
||||
var is_ce2 := _viewed_level == "CE2"
|
||||
_backdrop.kind = "marsh" if is_ce2 else "forest"
|
||||
AudioManager.play_music(SoundCatalog.music_for_level(_viewed_level))
|
||||
AudioManager.play_region_music(_viewed_level)
|
||||
_title_label.text = UIText.get_text("world_map.title_ce2" if is_ce2 else "world_map.title")
|
||||
_level_toggle_button.text = UIText.get_text(
|
||||
"world_map.review_ce1_button" if is_ce2 else "world_map.back_to_ce2_button"
|
||||
|
||||
@@ -7,7 +7,10 @@ const SFX_DIR := "res://assets/audio/sfx"
|
||||
const MUSIC_DIR := "res://assets/audio/music"
|
||||
|
||||
const SFX_IDS: Array[String] = ["click", "success", "try_again", "star", "fanfare"]
|
||||
const MUSIC_IDS: Array[String] = ["forest", "marsh"]
|
||||
const MUSIC_IDS: Array[String] = ["forest", "marsh", "meadow", "river", "night"]
|
||||
|
||||
## Choix « automatique » : la musique de la region (voir music_for_level).
|
||||
const MUSIC_AUTO := "auto"
|
||||
|
||||
|
||||
static func sfx_path(sfx_id: String) -> String:
|
||||
@@ -25,3 +28,17 @@ static func music_path(music_id: String) -> String:
|
||||
## Musique d'une region selon le niveau scolaire de la regle.
|
||||
static func music_for_level(level: String) -> String:
|
||||
return "marsh" if level == "CE2" else "forest"
|
||||
|
||||
|
||||
## Choix possibles dans les reglages : automatique puis chaque morceau.
|
||||
static func music_choices() -> Array[String]:
|
||||
var choices: Array[String] = [MUSIC_AUTO]
|
||||
choices.append_array(MUSIC_IDS)
|
||||
return choices
|
||||
|
||||
|
||||
## Morceau a jouer pour le choix de l'utilisateur et le niveau scolaire.
|
||||
static func resolve_music(choice: String, level: String) -> String:
|
||||
if MUSIC_IDS.has(choice):
|
||||
return choice
|
||||
return music_for_level(level)
|
||||
|
||||
@@ -22,6 +22,7 @@ static func defaults() -> Dictionary:
|
||||
"volume": 0.8,
|
||||
"music_volume": 0.5,
|
||||
"effects_volume": 0.8,
|
||||
"music_track": SoundCatalog.MUSIC_AUTO,
|
||||
"text_scale_index": 0,
|
||||
"animations_enabled": true,
|
||||
}
|
||||
@@ -41,6 +42,9 @@ static func sanitize(raw: Variant) -> Dictionary:
|
||||
var other: Variant = source.get(key)
|
||||
if typeof(other) == TYPE_FLOAT or typeof(other) == TYPE_INT:
|
||||
result[key] = clampf(float(other), 0.0, 1.0)
|
||||
var track: Variant = source.get("music_track")
|
||||
if typeof(track) == TYPE_STRING and SoundCatalog.music_choices().has(track):
|
||||
result["music_track"] = track
|
||||
var index: Variant = source.get("text_scale_index")
|
||||
if typeof(index) == TYPE_FLOAT or typeof(index) == TYPE_INT:
|
||||
result["text_scale_index"] = clampi(int(index), 0, TEXT_SCALES.size() - 1)
|
||||
|
||||
@@ -23,6 +23,23 @@ func test_music_follows_the_level() -> void:
|
||||
assert_eq(SoundCatalog.music_for_level("CE2"), "marsh")
|
||||
|
||||
|
||||
func test_there_are_five_music_tracks_and_an_automatic_choice() -> void:
|
||||
assert_eq(SoundCatalog.MUSIC_IDS.size(), 5)
|
||||
assert_eq(SoundCatalog.music_choices().size(), 6)
|
||||
assert_eq(SoundCatalog.music_choices()[0], SoundCatalog.MUSIC_AUTO)
|
||||
|
||||
|
||||
func test_resolve_music_follows_choice_or_level() -> void:
|
||||
assert_eq(SoundCatalog.resolve_music("night", "CE1"), "night")
|
||||
assert_eq(SoundCatalog.resolve_music("auto", "CE2"), "marsh")
|
||||
assert_eq(SoundCatalog.resolve_music("n_importe_quoi", "CE1"), "forest")
|
||||
|
||||
|
||||
func test_every_music_choice_has_a_label() -> void:
|
||||
for choice in SoundCatalog.music_choices():
|
||||
assert_ne(UIText.get_text("music.%s" % choice), "music.%s" % choice)
|
||||
|
||||
|
||||
func test_audio_manager_plays_without_crashing_and_remembers_music() -> void:
|
||||
AudioManager.play_sfx("success")
|
||||
AudioManager.play_music("marsh")
|
||||
|
||||
@@ -42,3 +42,9 @@ func test_music_and_effects_volumes_are_bounded() -> void:
|
||||
assert_eq(clean["music_volume"], 1.0)
|
||||
assert_eq(clean["effects_volume"], 0.0)
|
||||
assert_eq(AppSettings.sanitize({})["music_volume"], AppSettings.defaults()["music_volume"])
|
||||
|
||||
|
||||
func test_music_track_must_be_a_known_choice() -> void:
|
||||
assert_eq(AppSettings.sanitize({"music_track": "river"})["music_track"], "river")
|
||||
assert_eq(AppSettings.sanitize({"music_track": "../x"})["music_track"], "auto")
|
||||
assert_eq(AppSettings.sanitize({"music_track": 3})["music_track"], "auto")
|
||||
|
||||
@@ -68,7 +68,11 @@ def normalise(signal: Signal, peak: float) -> Signal:
|
||||
|
||||
def hz(note: str) -> float:
|
||||
names = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
||||
pitch = names[note[0]] + (1 if note[1] == "#" else 0)
|
||||
pitch = names[note[0]]
|
||||
if note[1] == "#":
|
||||
pitch += 1
|
||||
elif note[1] == "b":
|
||||
pitch -= 1
|
||||
octave = int(note[-1])
|
||||
midi = 12 * (octave + 1) + pitch
|
||||
return 440.0 * 2 ** ((midi - 69) / 12)
|
||||
@@ -149,7 +153,28 @@ def make_all_music() -> dict[str, Signal]:
|
||||
seed=23,
|
||||
melody_density=0.35,
|
||||
)
|
||||
return {"forest": forest, "marsh": marsh}
|
||||
meadow = make_music( # prairie : majeur lumineux, un peu plus vif
|
||||
chords=[["G2", "D3", "B3"], ["E2", "B2", "G3"], ["C3", "G3", "E4"], ["D3", "A3", "F#4"]] * 2,
|
||||
melody_scale=["G4", "A4", "B4", "D5", "E5", "G5"],
|
||||
seconds_per_chord=3.5,
|
||||
seed=37,
|
||||
melody_density=0.55,
|
||||
)
|
||||
river = make_music( # riviere : mode dorien, melodie qui coule
|
||||
chords=[["D3", "A3", "F4"], ["G2", "D3", "B3"], ["C3", "G3", "E4"], ["A2", "E3", "C4"]] * 2,
|
||||
melody_scale=["D5", "E5", "F5", "A5", "C6", "A4"],
|
||||
seconds_per_chord=3.0,
|
||||
seed=41,
|
||||
melody_density=0.65,
|
||||
)
|
||||
night = make_music( # nuit : berceuse lente et rare
|
||||
chords=[["F2", "C3", "A3"], ["D3", "A3", "E4"], ["Bb2", "F3", "D4"], ["C3", "G3", "E4"]] * 2,
|
||||
melody_scale=["F5", "G5", "A5", "C6", "E5"],
|
||||
seconds_per_chord=6.0,
|
||||
seed=53,
|
||||
melody_density=0.25,
|
||||
)
|
||||
return {"forest": forest, "marsh": marsh, "meadow": meadow, "river": river, "night": night}
|
||||
|
||||
|
||||
def write_ogg(signal: Signal, destination: Path, quality: str = "3") -> None:
|
||||
|
||||
Reference in New Issue
Block a user