Add time-varying pitch modulation and built-in procedural glitch effects

This commit is contained in:
rardog
2026-09-24 13:29:51 +03:00
parent 2b9c376bbc
commit cd3672a274
6 changed files with 460 additions and 2 deletions
@@ -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) {
@@ -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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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<SoundPlayerContainer> {
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()) {
@@ -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;
}
}
@@ -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 extends Enum<E>> E parse(String raw, E fallback) {
@@ -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);
}
}
@@ -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);