From cd3672a274b7041aad42ab92a53feb37d41abf6f Mon Sep 17 00:00:00 2001 From: rardog Date: Thu, 24 Sep 2026 13:29:51 +0300 Subject: [PATCH] Add time-varying pitch modulation and built-in procedural glitch effects --- .../client/audio/AudioPlaybackService.java | 57 ++++ .../client/screen/SoundPlayerScreen.java | 38 ++- .../soundmod/common/PitchModulationMode.java | 20 ++ .../soundmod/common/SoundSettings.java | 24 ++ .../server/AudioEffectGenerator.java | 320 ++++++++++++++++++ .../sounddata/server/WorldSoundIndex.java | 3 +- 6 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/soundmod/common/PitchModulationMode.java create mode 100644 src/main/java/com/example/soundmod/sounddata/server/AudioEffectGenerator.java diff --git a/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java b/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java index 6cb7262..9dc04b1 100644 --- a/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java +++ b/src/main/java/com/example/soundmod/client/audio/AudioPlaybackService.java @@ -23,6 +23,7 @@ package com.example.soundmod.client.audio; import com.example.soundmod.SoundMod; import com.example.soundmod.common.AttenuationMode; +import com.example.soundmod.common.PitchModulationMode; import com.example.soundmod.common.SoundSettings; import com.example.soundmod.core.ModConfig; import com.example.soundmod.core.SoundDebug; @@ -235,6 +236,7 @@ public final class AudioPlaybackService { dominantCategory = src.settings.category; } for (PlayingSource src : this.active.values()) { + src.elapsedTicks++; int state = AL10.alGetSourcei((int)src.source, (int)4112); if (SoundDebug.enabled(SoundDebug.Level.L3)) { SoundDebug.log(SoundDebug.Level.L3, "PLAYBACK", src.sourceId, "tick state={} gainTarget={}", state, Float.valueOf(src.targetGain)); @@ -250,6 +252,7 @@ public final class AudioPlaybackService { gain = this.applyPriorityDucking(src, gain, topPriority, dominantCategory); AL10.alSourcef((int)src.source, (int)4106, (float)Math.max(0.0f, gain)); float pitch = src.settings.pitch; + pitch *= this.computePitchModulation(src); if (src.settings.occlusion && src.settings.use3d && occlusionFactor < 0.999f && !"voice".equals(src.settings.category)) { pitch *= Math.max(0.9f, 1.0f - src.settings.occlusionStrength * 0.12f); } @@ -261,16 +264,69 @@ public final class AudioPlaybackService { float dopplerPitch = (float)Math.max(0.75, Math.min(1.25, 1.0 + radial * 0.04)); pitch *= dopplerPitch; } + pitch = Math.max(0.05f, Math.min(4.0f, pitch)); AL10.alSourcef((int)src.source, (int)4099, (float)pitch); if (src.reverbSource <= 0) continue; float wetGain = Math.max(0.0f, gain * src.settings.reverbStrength * 0.45f); AL10.alSourcef((int)src.reverbSource, (int)4106, (float)wetGain); + AL10.alSourcef((int)src.reverbSource, (int)4099, (float)Math.max(0.05f, Math.min(4.0f, pitch * 0.97f))); } for (String id : remove) { this.stop(id); } } + private float computePitchModulation(PlayingSource src) { + if (src.settings.pitchMode == null || src.settings.pitchMode == PitchModulationMode.STATIC) { + return 1.0f; + } + float speed = Math.max(0.05f, src.settings.pitchModSpeed); + float depth = Math.max(0.0f, src.settings.pitchModDepth); + float t = (float) src.elapsedTicks / 20.0f; + + switch (src.settings.pitchMode) { + case VIBRATO: { + float freq = speed * 5.0f; + return 1.0f + (float) Math.sin(t * freq * (2.0 * Math.PI)) * (depth * 0.4f); + } + case GLITCH: { + int stepTicks = Math.max(1, (int)(8.0f / speed)); + int step = src.elapsedTicks / stepTicks; + int hash = (step * 0x1f1f1f1f) ^ (src.sourceId.hashCode() * 31); + int choice = Math.abs(hash) % 7; + float glitchFactor = 1.0f; + switch (choice) { + case 0: glitchFactor = 0.5f; break; + case 1: glitchFactor = 0.75f; break; + case 2: glitchFactor = 1.25f; break; + case 3: glitchFactor = 1.5f; break; + case 4: glitchFactor = 2.0f; break; + case 5: glitchFactor = 0.9f; break; + case 6: glitchFactor = 1.15f; break; + } + return 1.0f + (glitchFactor - 1.0f) * Math.min(1.5f, depth); + } + case TAPE_STOP: { + float progress = Math.min(1.0f, t * speed * 0.2f); + float decay = (1.0f - progress); + float factor = 1.0f - (1.0f - decay * decay) * Math.min(1.0f, depth); + return Math.max(0.1f, factor); + } + case RAMP_UP: { + float progress = t * speed * 0.25f; + return Math.min(4.0f, 1.0f + progress * depth); + } + case RANDOM_JITTER: { + float wow = (float) Math.sin(t * speed * 1.8f * Math.PI) * 0.5f; + int noiseHash = (src.elapsedTicks * 1103515245 + 12345); + float flutter = (((noiseHash >> 16) & 0x7fff) / 32767.0f - 0.5f); + return 1.0f + (wow * 0.7f + flutter * 0.3f) * (depth * 0.25f); + } + default: + return 1.0f; + } + } + private float applyFades(PlayingSource src, float gain) { if (src.fadeInTicks > 0) { int elapsed = src.settings.fadeInTicks - src.fadeInTicks; @@ -596,6 +652,7 @@ public final class AudioPlaybackService { int fadeInTicks; int fadeOutTicks; int reverbSource; + int elapsedTicks; long lastLiveUpdateTick = Long.MIN_VALUE; PlayingSource(String sourceId, int source, int buffer, BlockPos pos, SoundSettings settings, long startedAtTick) { diff --git a/src/main/java/com/example/soundmod/client/screen/SoundPlayerScreen.java b/src/main/java/com/example/soundmod/client/screen/SoundPlayerScreen.java index b1861af..fde1307 100644 --- a/src/main/java/com/example/soundmod/client/screen/SoundPlayerScreen.java +++ b/src/main/java/com/example/soundmod/client/screen/SoundPlayerScreen.java @@ -3,6 +3,7 @@ package com.example.soundmod.client.screen; import com.example.soundmod.client.audio.AudioPlaybackService; import com.example.soundmod.common.ActivationMode; import com.example.soundmod.common.AttenuationMode; +import com.example.soundmod.common.PitchModulationMode; import com.example.soundmod.common.RedstoneMode; import com.example.soundmod.common.ScenarioQueue; import com.example.soundmod.common.SoundManifestEntry; @@ -111,6 +112,9 @@ public class SoundPlayerScreen extends ContainerScreen { private Button crossfadeButton; private TextFieldWidget fadeInField; private TextFieldWidget fadeOutField; + private Button pitchModeButton; + private TextFieldWidget pitchModSpeedField; + private TextFieldWidget pitchModDepthField; // Controls - Trigger private Button activationModeButton; @@ -349,6 +353,14 @@ public class SoundPlayerScreen extends ContainerScreen { this.fadeInField = this.createField(x + 88, y + 147, 44, Integer.toString(this.localSettings.fadeInTicks)); this.fadeOutField = this.createField(x + 226, y + 147, 44, Integer.toString(this.localSettings.fadeOutTicks)); + this.pitchModeButton = this.addButtonTracked(new Button(x, y + 172, 114, 20, new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName()), b -> { + this.localSettings.pitchMode = this.cyclePitchMode(this.localSettings.pitchMode); + b.setMessage(new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName())); + this.sendLiveApply("pitchMode"); + }), this.effectsButtons); + this.pitchModSpeedField = this.createField(x + 152, y + 173, 34, this.floatText(this.localSettings.pitchModSpeed, 1)); + this.pitchModDepthField = this.createField(x + 228, y + 173, 34, this.floatText(this.localSettings.pitchModDepth, 2)); + // --- TAB 3: TRIGGER --- this.activationModeButton = this.addButtonTracked(new Button(x, y + 42, 128, 20, new StringTextComponent("Режим: " + this.modeRu(this.localSettings.activationMode)), b -> { this.localSettings.activationMode = this.cycleMode(this.localSettings.activationMode); @@ -478,6 +490,8 @@ public class SoundPlayerScreen extends ContainerScreen { this.addFieldTracked(this.reverbStrengthField, this.effectsFields); this.addFieldTracked(this.fadeInField, this.effectsFields); this.addFieldTracked(this.fadeOutField, this.effectsFields); + this.addFieldTracked(this.pitchModSpeedField, this.effectsFields); + this.addFieldTracked(this.pitchModDepthField, this.effectsFields); this.addFieldTracked(this.startDelayField, this.triggerFields); this.addFieldTracked(this.repeatDelayField, this.triggerFields); @@ -741,6 +755,8 @@ public class SoundPlayerScreen extends ContainerScreen { if (this.reverbStrengthField != null) this.localSettings.reverbStrength = this.clampFloat(this.parseFloat(this.reverbStrengthField.getValue(), 0.35f), 0.0f, 1.0f); if (this.fadeInField != null) this.localSettings.fadeInTicks = this.clampInt(this.parseInt(this.fadeInField.getValue(), 0), 0, 72000); if (this.fadeOutField != null) this.localSettings.fadeOutTicks = this.clampInt(this.parseInt(this.fadeOutField.getValue(), 0), 0, 72000); + if (this.pitchModSpeedField != null) this.localSettings.pitchModSpeed = this.clampFloat(this.parseFloat(this.pitchModSpeedField.getValue(), 1.0f), 0.05f, 20.0f); + if (this.pitchModDepthField != null) this.localSettings.pitchModDepth = this.clampFloat(this.parseFloat(this.pitchModDepthField.getValue(), 0.25f), 0.0f, 2.0f); if (this.startDelayField != null) this.localSettings.startDelayTicks = this.clampInt(this.parseInt(this.startDelayField.getValue(), 0), 0, 72000); if (this.repeatDelayField != null) this.localSettings.repeatDelayTicks = this.clampInt(this.parseInt(this.repeatDelayField.getValue(), 0), 0, 72000); @@ -792,6 +808,15 @@ public class SoundPlayerScreen extends ContainerScreen { this.zoneStopOnExitButton.setMessage(this.boolLabel("Стоп-выход", this.zoneStopOnExit)); this.zoneUseCustomCoordsButton.setMessage(this.boolLabel("Смещение центра", this.zoneUseCustomCoords)); this.scenarioLoopButton.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop())); + if (this.pitchModSpeedField != null) { + this.pitchModSpeedField.setValue(this.floatText(this.localSettings.pitchModSpeed, 1)); + } + if (this.pitchModDepthField != null) { + this.pitchModDepthField.setValue(this.floatText(this.localSettings.pitchModDepth, 2)); + } + if (this.pitchModeButton != null) { + this.pitchModeButton.setMessage(new StringTextComponent("Pitch: " + this.localSettings.pitchMode.getDisplayName())); + } if (this.scenarioShuffleButton != null) { this.scenarioShuffleButton.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle())); } @@ -801,6 +826,12 @@ public class SoundPlayerScreen extends ContainerScreen { this.updateTabVisibility(); } + private PitchModulationMode cyclePitchMode(PitchModulationMode current) { + PitchModulationMode[] values = PitchModulationMode.values(); + int next = (current.ordinal() + 1) % values.length; + return values[next]; + } + private void addScenarioStep() { String sid = this.scenarioSoundField.getValue().trim(); if (sid.isEmpty()) { @@ -1113,7 +1144,9 @@ public class SoundPlayerScreen extends ContainerScreen { this.font.draw(matrixStack, new StringTextComponent("Сила:"), (float) (x + 176), (float) (y + 99), COLOR_TEXT_LABEL); this.font.draw(matrixStack, new StringTextComponent("Fade In:"), (float) x, (float) (y + 151), COLOR_TEXT_LABEL); this.font.draw(matrixStack, new StringTextComponent("Fade Out:"), (float) (x + 148), (float) (y + 151), COLOR_TEXT_LABEL); - this.font.draw(matrixStack, new StringTextComponent("⚡ Изменения применяются мгновенно во время звука"), (float) x, (float) (y + 176), COLOR_TEXT_MUTED); + this.font.draw(matrixStack, new StringTextComponent("Скор:"), (float) (x + 118), (float) (y + 177), COLOR_TEXT_LABEL); + this.font.draw(matrixStack, new StringTextComponent("Глуб:"), (float) (x + 192), (float) (y + 177), COLOR_TEXT_LABEL); + this.font.draw(matrixStack, new StringTextComponent("⚡ Изменения применяются на лету во время звука"), (float) x, (float) (y + 195), COLOR_TEXT_MUTED); break; case TRIGGER: @@ -1321,6 +1354,7 @@ public class SoundPlayerScreen extends ContainerScreen { lines.add("• ▶ Тест: мгновенное предпрослушивание выбранного звука."); lines.add("• 2x клик по списку: запускает предпрослушивание трека."); lines.add("• Эффекты: 3D звук, приглушение за стенами (окклюзия), эхо."); + lines.add("• Модуляция Pitch: вибрато, глитч, tape stop (замедление), разгон."); lines.add("• Триггер: настройка запуска редстоуном, кликом или таймером."); lines.add("• Зона: музыка включается при входе в зону и гаснет при выходе."); lines.add("• Вид зоны: отображает 3D границы зоны прямо в игровом мире."); @@ -1467,6 +1501,8 @@ public class SoundPlayerScreen extends ContainerScreen { this.renderTooltip(matrixStack, new StringTextComponent("Вставить текущие координаты игрока как центр звука"), mouseX, mouseY); } else if (this.zoneVisualizerButton != null && this.zoneVisualizerButton.isHovered()) { this.renderTooltip(matrixStack, new StringTextComponent("Отображать 3D границы зоны в игровом мире"), mouseX, mouseY); + } else if (this.pitchModeButton != null && this.pitchModeButton.isHovered()) { + this.renderTooltip(matrixStack, new StringTextComponent("Динамическое изменение тона звука во времени (Вибрато, Глитч, Tape Stop и др.)"), mouseX, mouseY); } else if (this.copyConfigButton != null && this.copyConfigButton.isHovered()) { this.renderTooltip(matrixStack, new StringTextComponent("Скопировать все настройки блока в буфер обмена"), mouseX, mouseY); } else if (this.pasteConfigButton != null && this.pasteConfigButton.isHovered()) { diff --git a/src/main/java/com/example/soundmod/common/PitchModulationMode.java b/src/main/java/com/example/soundmod/common/PitchModulationMode.java new file mode 100644 index 0000000..29dbf03 --- /dev/null +++ b/src/main/java/com/example/soundmod/common/PitchModulationMode.java @@ -0,0 +1,20 @@ +package com.example.soundmod.common; + +public enum PitchModulationMode { + STATIC("Обычный"), + VIBRATO("Вибрато"), + GLITCH("Глитч"), + TAPE_STOP("Tape Stop (Замедл.)"), + RAMP_UP("Ramp Up (Ускор.)"), + RANDOM_JITTER("Дрожание"); + + private final String displayName; + + PitchModulationMode(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return this.displayName; + } +} diff --git a/src/main/java/com/example/soundmod/common/SoundSettings.java b/src/main/java/com/example/soundmod/common/SoundSettings.java index 45da1b2..8eb1b6c 100644 --- a/src/main/java/com/example/soundmod/common/SoundSettings.java +++ b/src/main/java/com/example/soundmod/common/SoundSettings.java @@ -33,6 +33,9 @@ public class SoundSettings { public boolean crossfade; public ActivationMode activationMode = ActivationMode.CLICK; public RedstoneMode redstoneMode = RedstoneMode.EDGE; + public PitchModulationMode pitchMode = PitchModulationMode.STATIC; + public float pitchModSpeed = 1.0f; + public float pitchModDepth = 0.25f; public CompoundNBT toNbt() { CompoundNBT tag = new CompoundNBT(); @@ -57,6 +60,9 @@ public class SoundSettings { tag.putBoolean("crossfade", this.crossfade); tag.putString("activationMode", this.activationMode.name()); tag.putString("redstoneMode", this.redstoneMode.name()); + tag.putString("pitchMode", this.pitchMode.name()); + tag.putFloat("pitchModSpeed", this.pitchModSpeed); + tag.putFloat("pitchModDepth", this.pitchModDepth); return tag; } @@ -125,6 +131,15 @@ public class SoundSettings { if (tag.contains("redstoneMode")) { s.redstoneMode = SoundSettings.parse(tag.getString("redstoneMode"), RedstoneMode.EDGE); } + if (tag.contains("pitchMode")) { + s.pitchMode = SoundSettings.parse(tag.getString("pitchMode"), PitchModulationMode.STATIC); + } + if (tag.contains("pitchModSpeed")) { + s.pitchModSpeed = tag.getFloat("pitchModSpeed"); + } + if (tag.contains("pitchModDepth")) { + s.pitchModDepth = tag.getFloat("pitchModDepth"); + } SoundSettings.sanitizeInPlace(s); return s; } @@ -175,6 +190,15 @@ public class SoundSettings { if (s.redstoneMode == null) { s.redstoneMode = RedstoneMode.EDGE; } + if (s.pitchMode == null) { + s.pitchMode = PitchModulationMode.STATIC; + } + if (!Float.isFinite(s.pitchModSpeed) || s.pitchModSpeed < 0.05f || s.pitchModSpeed > 20.0f) { + s.pitchModSpeed = 1.0f; + } + if (!Float.isFinite(s.pitchModDepth) || s.pitchModDepth < 0.0f || s.pitchModDepth > 2.0f) { + s.pitchModDepth = 0.25f; + } } private static > E parse(String raw, E fallback) { diff --git a/src/main/java/com/example/soundmod/sounddata/server/AudioEffectGenerator.java b/src/main/java/com/example/soundmod/sounddata/server/AudioEffectGenerator.java new file mode 100644 index 0000000..4b34d4f --- /dev/null +++ b/src/main/java/com/example/soundmod/sounddata/server/AudioEffectGenerator.java @@ -0,0 +1,320 @@ +package com.example.soundmod.sounddata.server; + +import com.example.soundmod.SoundMod; +import com.example.soundmod.core.SoundDebug; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +public final class AudioEffectGenerator { + private static final int SAMPLE_RATE = 44100; + + private AudioEffectGenerator() { + } + + public static void generatePresetsIfMissing(Path soundsRoot) { + try { + Files.createDirectories(soundsRoot); + generateIfMissing(soundsRoot.resolve("glitch_stutter.wav"), AudioEffectGenerator::generateGlitchStutter); + generateIfMissing(soundsRoot.resolve("glitch_data_burst.wav"), AudioEffectGenerator::generateDataBurst); + generateIfMissing(soundsRoot.resolve("radio_static.wav"), AudioEffectGenerator::generateRadioStatic); + generateIfMissing(soundsRoot.resolve("cyber_sweep.wav"), AudioEffectGenerator::generateCyberSweep); + generateIfMissing(soundsRoot.resolve("tape_stop_fx.wav"), AudioEffectGenerator::generateTapeStop); + generateIfMissing(soundsRoot.resolve("bass_glitch_drop.wav"), AudioEffectGenerator::generateBassGlitchDrop); + } catch (Exception e) { + SoundMod.LOGGER.error("Failed to generate preset audio effects", e); + SoundDebug.warn(SoundDebug.Level.L1, "PRESET", "GEN", "failed to generate audio presets: {}", e.getMessage()); + } + } + + private static void generateIfMissing(Path file, Generator generator) { + if (Files.exists(file)) { + return; + } + try { + short[] pcm = generator.generate(); + writeWav(file, pcm); + SoundMod.LOGGER.info("Generated preset sound effect: {}", file.getFileName()); + SoundDebug.log(SoundDebug.Level.L1, "PRESET", "GEN", "created preset {}", file.getFileName()); + } catch (IOException e) { + SoundMod.LOGGER.warn("Could not write preset sound {}: {}", file.getFileName(), e.getMessage()); + } + } + + @FunctionalInterface + private interface Generator { + short[] generate(); + } + + private static void writeWav(Path file, short[] samples) throws IOException { + int numChannels = 1; + int bitsPerSample = 16; + int byteRate = SAMPLE_RATE * numChannels * (bitsPerSample / 8); + int blockAlign = numChannels * (bitsPerSample / 8); + int dataSize = samples.length * 2; + int chunkSize = 36 + dataSize; + + ByteBuffer buffer = ByteBuffer.allocate(44 + dataSize).order(ByteOrder.LITTLE_ENDIAN); + buffer.put(new byte[]{'R', 'I', 'F', 'F'}); + buffer.putInt(chunkSize); + buffer.put(new byte[]{'W', 'A', 'V', 'E'}); + buffer.put(new byte[]{'f', 'm', 't', ' '}); + buffer.putInt(16); + buffer.putShort((short) 1); + buffer.putShort((short) numChannels); + buffer.putInt(SAMPLE_RATE); + buffer.putInt(byteRate); + buffer.putShort((short) blockAlign); + buffer.putShort((short) bitsPerSample); + buffer.put(new byte[]{'d', 'a', 't', 'a'}); + buffer.putInt(dataSize); + + for (short sample : samples) { + buffer.putShort(sample); + } + + Files.write(file, buffer.array()); + } + + /** + * Stuttering digital glitch with buffer repeats, bitcrushing, and frequency hops. + */ + private static short[] generateGlitchStutter() { + int durationSeconds = 3; + int totalSamples = SAMPLE_RATE * durationSeconds; + short[] out = new short[totalSamples]; + Random rnd = new Random(1337); + + int grainLength = (int) (SAMPLE_RATE * 0.065); // 65ms grain + double baseFreq = 220.0; + double phase = 0.0; + + for (int i = 0; i < totalSamples; ) { + int repeats = 3 + rnd.nextInt(5); + double freqMult; + switch (rnd.nextInt(5)) { + case 0: freqMult = 0.5; break; + case 1: freqMult = 1.0; break; + case 2: freqMult = 1.5; break; + case 3: freqMult = 2.0; break; + default: freqMult = 0.75; break; + } + double grainFreq = baseFreq * freqMult; + boolean isNoise = rnd.nextFloat() < 0.25f; + + for (int r = 0; r < repeats && i < totalSamples; r++) { + for (int g = 0; g < grainLength && i < totalSamples; g++, i++) { + double t = (double) g / grainLength; + // Envelope for grain to prevent harsh clicks + double env = Math.sin(t * Math.PI); + double val; + if (isNoise) { + val = (rnd.nextDouble() * 2.0 - 1.0) * env; + } else { + phase += (2.0 * Math.PI * grainFreq) / SAMPLE_RATE; + val = Math.sin(phase) * env; + // Add some square harmonics + val += 0.3 * Math.signum(Math.sin(phase * 2.0)) * env; + } + // Bitcrush effect + int quantized = (int) (val * 8.0); + val = (double) quantized / 8.0; + + out[i] = clampSample(val * 0.8); + } + } + } + return out; + } + + /** + * High-speed sci-fi data packet burst (FSK modem chirps and cyber pulses). + */ + private static short[] generateDataBurst() { + int totalSamples = (int) (SAMPLE_RATE * 1.8); + short[] out = new short[totalSamples]; + Random rnd = new Random(4242); + + int baudRate = 120; // pulses per second + int samplesPerBit = SAMPLE_RATE / baudRate; + double phase = 0.0; + + for (int i = 0; i < totalSamples; i++) { + int bitIndex = i / samplesPerBit; + rnd.setSeed(bitIndex * 31337L + 7); + double freq = 1200.0 + (rnd.nextInt(8) * 400.0); + if (rnd.nextFloat() < 0.15f) { + freq = 4400.0; + } + + phase += (2.0 * Math.PI * freq) / SAMPLE_RATE; + double sample = Math.sin(phase); + + // Resonant pulse shape + double bitT = (double) (i % samplesPerBit) / samplesPerBit; + double env = Math.sin(bitT * Math.PI); + + // Add high noise texture + double noise = (rnd.nextDouble() * 2.0 - 1.0) * 0.12; + + double val = (sample * 0.75 + noise) * env; + out[i] = clampSample(val * 0.85); + } + return out; + } + + /** + * Atmospheric radio tuning static, analog white noise and vinyl pops. + */ + private static short[] generateRadioStatic() { + int totalSamples = SAMPLE_RATE * 4; + short[] out = new short[totalSamples]; + Random rnd = new Random(999); + + double lpState = 0.0; + double sweepPhase = 0.0; + + for (int i = 0; i < totalSamples; i++) { + double progress = (double) i / totalSamples; + // White noise + double white = rnd.nextDouble() * 2.0 - 1.0; + + // Simple 1-pole low-pass filter with moving cutoff + double cutoff = 0.08 + 0.15 * Math.sin(progress * Math.PI * 4.0); + lpState += cutoff * (white - lpState); + + // Radio heterodyne whistle (whistling tone that changes pitch) + double whistleFreq = 800.0 + 1200.0 * Math.sin(progress * Math.PI * 3.0); + sweepPhase += (2.0 * Math.PI * whistleFreq) / SAMPLE_RATE; + double whistle = Math.sin(sweepPhase) * 0.15; + + // Vinyl / static pops + double pop = 0.0; + if (rnd.nextFloat() < 0.003f) { + pop = (rnd.nextDouble() * 2.0 - 1.0) * 0.7; + } + + double val = lpState * 0.6 + whistle + pop; + out[i] = clampSample(val * 0.8); + } + return out; + } + + /** + * Cinematic cyber pitch sweep / riser with laser sheen. + */ + private static short[] generateCyberSweep() { + int totalSamples = (int) (SAMPLE_RATE * 2.5); + short[] out = new short[totalSamples]; + + double phase = 0.0; + double subPhase = 0.0; + + for (int i = 0; i < totalSamples; i++) { + double t = (double) i / totalSamples; + // Exponential frequency rise from 100Hz to 4800Hz + double freq = 100.0 * Math.pow(48.0, t); + phase += (2.0 * Math.PI * freq) / SAMPLE_RATE; + + // Sub bass punch at beginning decaying + double subFreq = 80.0 * (1.0 - t * 0.5); + subPhase += (2.0 * Math.PI * subFreq) / SAMPLE_RATE; + double sub = Math.sin(subPhase) * Math.max(0.0, 1.0 - t * 2.5); + + // Main riser tone with saturation + double wave = Math.sin(phase) + 0.3 * Math.sin(phase * 2.0); + // Envelope rises towards end with quick fadeout + double env = Math.pow(t, 1.5) * (1.0 - Math.pow(t, 16.0)); + + double val = (wave * env * 0.7) + (sub * 0.6); + out[i] = clampSample(val * 0.85); + } + return out; + } + + /** + * Warm tape machine motor stop deceleration. + */ + private static short[] generateTapeStop() { + int totalSamples = (int) (SAMPLE_RATE * 2.2); + short[] out = new short[totalSamples]; + + double[] chord = new double[]{220.0, 261.63, 329.63, 392.0}; // A minor 7th + double[] phases = new double[chord.length]; + + for (int i = 0; i < totalSamples; i++) { + double t = (double) i / totalSamples; + // Deceleration curve: normal speed for 20%, then steep pitch drop + double pitchFactor; + if (t < 0.2) { + pitchFactor = 1.0; + } else { + double decelT = (t - 0.2) / 0.8; + pitchFactor = Math.max(0.02, Math.pow(1.0 - decelT, 3.0)); + } + + double sum = 0.0; + for (int c = 0; c < chord.length; c++) { + phases[c] += (2.0 * Math.PI * chord[c] * pitchFactor) / SAMPLE_RATE; + sum += Math.sin(phases[c]); + } + sum /= chord.length; + + // Fade out as it stops + double amp = Math.max(0.0, 1.0 - Math.pow(t, 2.5)); + // Soft tape saturation + double saturated = Math.tanh(sum * 1.5); + + out[i] = clampSample(saturated * amp * 0.85); + } + return out; + } + + /** + * Heavy 808 sub-bass hit morphing into bitcrushed digital glitch tail. + */ + private static short[] generateBassGlitchDrop() { + int totalSamples = (int) (SAMPLE_RATE * 2.2); + short[] out = new short[totalSamples]; + Random rnd = new Random(808); + + double subPhase = 0.0; + double noisePhase = 0.0; + + for (int i = 0; i < totalSamples; i++) { + double t = (double) i / totalSamples; + // 808 pitch drop from 150Hz to 45Hz + double freq = 45.0 + 105.0 * Math.exp(-t * 12.0); + subPhase += (2.0 * Math.PI * freq) / SAMPLE_RATE; + + double sub = Math.sin(subPhase); + // Add warm 2nd harmonic + sub += 0.3 * Math.sin(subPhase * 2.0); + + // Sub envelope + double subEnv = Math.exp(-t * 2.5); + + // Glitch tail starts after 0.4s + double glitch = 0.0; + if (t > 0.35) { + double glitchT = (t - 0.35) / 0.65; + noisePhase += (2.0 * Math.PI * (300.0 + rnd.nextInt(600))) / SAMPLE_RATE; + glitch = Math.sin(noisePhase) * (rnd.nextFloat() < 0.3f ? 1.0 : 0.0); + glitch *= Math.sin(glitchT * Math.PI); + } + + double total = sub * subEnv + glitch * 0.4; + out[i] = clampSample(total * 0.9); + } + return out; + } + + private static short clampSample(double val) { + if (val > 1.0) val = 1.0; + if (val < -1.0) val = -1.0; + return (short) (val * 32767.0); + } +} diff --git a/src/main/java/com/example/soundmod/sounddata/server/WorldSoundIndex.java b/src/main/java/com/example/soundmod/sounddata/server/WorldSoundIndex.java index b1ca444..991cef9 100644 --- a/src/main/java/com/example/soundmod/sounddata/server/WorldSoundIndex.java +++ b/src/main/java/com/example/soundmod/sounddata/server/WorldSoundIndex.java @@ -36,6 +36,7 @@ public class WorldSoundIndex { SoundDebug.log(SoundDebug.Level.L1, "INDEX", "WORLD", "scan start soundsRoot={}", this.soundsRoot); try { Files.createDirectories(this.soundsRoot, new FileAttribute[0]); + AudioEffectGenerator.generatePresetsIfMissing(this.soundsRoot); } catch (IOException e) { SoundMod.LOGGER.error("Could not create customsounds folder {}", (Object)this.soundsRoot, (Object)e); @@ -92,7 +93,7 @@ public class WorldSoundIndex { SoundDebug.warn(SoundDebug.Level.L1, "INDEX", relative, "oversized size={} limit={}", size, ModConfig.MAX_SOUND_FILE_BYTES.get()); return; } - if (!"ogg".equals(ext)) { + if (!"ogg".equals(ext) && !"wav".equals(ext) && !"mp3".equals(ext)) { flags = 1; this.manifest.add(new SoundManifestEntry(WorldSoundIndex.soundIdFor(relative), relative, "", size, lastModified, flags)); SoundMod.LOGGER.warn("Unsupported sound format in v1: {}", (Object)relative);