Add time-varying pitch modulation and built-in procedural glitch effects
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user