OceanSound: L/R-Panning fix via Dual-Source + constant-power panning

Zwei AudioNodes pro Sound (L + R) fest auf ±90° der Kamera positioniert;
Lautstärkeverhältnis (constant-power) bestimmt die wahrgenommene Richtung.
Behebt HRTF-0°-Stille wenn Kamera aufs Wasser zeigt. Außerdem weitere
Soundsystem- und Editor-Korrekturen aus dieser Session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 22:22:06 +02:00
parent 66d7956a28
commit 4fd337aea4
9 changed files with 272 additions and 101 deletions

View File

@@ -1262,6 +1262,11 @@ public class EditorApp extends Application {
camOrbitBtn.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
camFreeBtn.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
Button camResetBtn = new Button("⌂ Reset");
camResetBtn.setStyle("-fx-font-weight:bold;");
camResetBtn.setTooltip(new javafx.scene.control.Tooltip("Kamera auf x=0, z=0, y=Terrain+10m zurücksetzen"));
camResetBtn.setOnAction(e -> input.resetCameraRequested.set(true));
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
hint.setStyle("-fx-text-fill: #555;");
@@ -1274,7 +1279,7 @@ public class EditorApp extends Application {
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn,
new Separator(Orientation.VERTICAL), playToolBtn,
new Separator(Orientation.VERTICAL), voxelBtn,
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn,
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn,
new Separator(Orientation.VERTICAL), hint);
worldToolBar = toolBar;
@@ -5094,12 +5099,18 @@ public class EditorApp extends Application {
catch (IOException ignored) {}
}
/** Konvertiert eine beliebige Audiodatei mit ffmpeg zu OGG Vorbis.
* Gibt den Pfad zur erzeugten .ogg-Datei zurück. */
/**
* Konvertiert / normalisiert eine Audiodatei zu OGG Vorbis bei 48000 Hz.
* Das Ziel-Format stimmt mit PipeWire/PulseAudio überein, sodass OpenALSofts
* Spatial-Mixer kein On-the-fly-Resampling durchführen muss (kein Knistern).
*/
private static Path convertToOgg(Path src, Path destOgg) throws IOException {
try {
Process proc = new ProcessBuilder(
"ffmpeg", "-i", src.toString(), "-q:a", "4", destOgg.toString(), "-y")
"ffmpeg", "-i", src.toString(),
"-ar", "48000",
"-q:a", "4",
destOgg.toString(), "-y")
.redirectErrorStream(true)
.start();
proc.getInputStream().transferTo(java.io.OutputStream.nullOutputStream());
@@ -5158,12 +5169,8 @@ public class EditorApp extends Application {
if (isAudio) {
String baseName = file.getName().replaceFirst("\\.[^.]+$", "");
Path destOgg = destDir.resolve(baseName + ".ogg");
if (name.endsWith(".ogg")) {
Files.copy(file.toPath(), destOgg, StandardCopyOption.REPLACE_EXISTING);
} else {
setStatus("Konvertiere " + file.getName() + " → OGG …");
convertToOgg(file.toPath(), destOgg);
}
setStatus("Normalisiere " + file.getName() + " → 48 kHz OGG …");
convertToOgg(file.toPath(), destOgg);
String finalName = baseName + ".ogg";
TreeItem<String> newItem = new TreeItem<>(finalName);
itemPaths.put(newItem, destOgg);
@@ -5716,12 +5723,8 @@ public class EditorApp extends Application {
String name = file.getName().toLowerCase();
String baseName = file.getName().replaceFirst("\\.[^.]+$", "");
Path dest = destDir.resolve(baseName + ".ogg");
if (name.endsWith(".ogg")) {
Files.copy(file.toPath(), dest, StandardCopyOption.REPLACE_EXISTING);
} else {
setStatus("Konvertiere " + file.getName() + " → OGG …");
convertToOgg(file.toPath(), dest);
}
setStatus("Normalisiere " + file.getName() + " → 48 kHz OGG …");
convertToOgg(file.toPath(), dest);
TreeItem<String> item = new TreeItem<>(dest.getFileName().toString());
itemPaths.put(item, dest);
audioNode.getChildren().add(item);

View File

@@ -49,6 +49,10 @@ public class SharedInput {
public static final int CAM_ORBIT = 0;
public static final int CAM_FREEFLY = 1;
/** Gesetzt von JavaFX; konsumiert von TerrainEditorState: Kamera auf x=0,z=0,y=terrain+10 zurücksetzen. */
public final java.util.concurrent.atomic.AtomicBoolean resetCameraRequested =
new java.util.concurrent.atomic.AtomicBoolean(false);
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
public volatile boolean forward, backward, left, right, up, down;

View File

@@ -1607,12 +1607,24 @@ public class TerrainEditorState extends BaseAppState {
if (scroll != 0)
camPos.addLocal(cam.getDirection().mult(scroll * FastMath.clamp(terrainDist, 5f, CAM_SPEED) * 0.02f));
// Kamera-Reset auf x=0,z=0,y=terrain+10
if (input.resetCameraRequested.getAndSet(false)) {
float h = getTerrainHeightFast(0f, 0f);
camPos.set(0f, h + 10f, 0f);
camYaw = 0f;
camPitch = DEFAULT_PITCH;
}
// NaN-Sanitierung (z.B. durch terrain.getHeight()-Anomalie propagiert)
if (!Float.isFinite(camPos.x) || !Float.isFinite(camPos.y) || !Float.isFinite(camPos.z)) {
camPos.set(0f, DEFAULT_CAM_Y, 0f);
}
camPos.y = FastMath.clamp(camPos.y, -200f, MAX_CAM_Y);
// Kamera nicht unter das Terrain fallen lassen
float terrainFloor = getTerrainHeightFast(camPos.x, camPos.z) + 2f;
if (camPos.y < terrainFloor) camPos.y = terrainFloor;
cam.setLocation(camPos);
}