78 lines
4.6 KiB
Python
78 lines
4.6 KiB
Python
"""Nature field recordings edited into planet ambience. Requires numpy + soundfile."""
|
|
from pathlib import Path
|
|
import sys
|
|
import json
|
|
import wave
|
|
import numpy as np
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0,str(ROOT/"tmp/surface_audio_tools"))
|
|
import soundfile as sf
|
|
from build_freighter_audio import eq, peak, pitch, fade, loop, RATE
|
|
|
|
SOURCE = ROOT/"tmp/surface_audio_sources"
|
|
OUT = ROOT/"assets/sounds/surface"
|
|
manifest = {}
|
|
|
|
def read(path, start=0, seconds=None):
|
|
with sf.SoundFile(SOURCE/path) as stream:
|
|
rate = stream.samplerate
|
|
stream.seek(int(start*rate))
|
|
signal = stream.read(-1 if seconds is None else int(seconds*rate),always_2d=True)
|
|
if signal.shape[1] == 1: signal = np.repeat(signal,2,axis=1)
|
|
signal = signal[:,:2]
|
|
if rate != RATE:
|
|
at = np.arange(int(len(signal)*RATE/rate))*rate/RATE
|
|
signal = np.stack([np.interp(at,np.arange(len(signal)),signal[:,c]) for c in range(2)],axis=1)
|
|
signal -= signal.mean(axis=0)
|
|
return signal
|
|
|
|
def save(name, x, sources, processing, looping=False):
|
|
x = peak(x,-3)
|
|
assert np.isfinite(x).all() and np.max(np.abs(x)) < .9
|
|
with wave.open(str(OUT/(name+".wav")),"wb") as stream:
|
|
stream.setparams((2,2,RATE,0,"NONE","not compressed"))
|
|
stream.writeframes((x*32767).astype("<i2").tobytes())
|
|
manifest[name] = {"sources":sources,"processing":processing,"loop":looping,"seconds":round(len(x)/RATE,3),"peak_dbfs":-3,"rms_dbfs":round(20*np.log10(np.sqrt((x*x).mean())),2)}
|
|
return x
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
# Avoid recording start/handling noise; remove rumble and distant bird highs.
|
|
wind = read("wind.wav",start=8,seconds=24)
|
|
wind = save("wind_loop",loop(eq(wind,low=45,high=1900),2),["wind.wav: 8..32 seconds"],"Real open-field wind; rumble/high-bird reduction; two-second seam crossfade",True)
|
|
gust = fade(eq(read("wind.wav",start=45,seconds=8),low=70,high=2900),2,3)
|
|
gust = save("wind_gust",gust,["wind.wav: 45..53 seconds"],"Natural gust with long soft attack/release and low-pass")
|
|
leaves = [eq(read("leaves/rustle/rustle%02d.flac"%i),low=180,high=5200) for i in [1,4,9]]
|
|
foliage = np.zeros((int(16*RATE),2))
|
|
for index in range(12):
|
|
leaf = fade(peak(leaves[index%3]),.25,.55)
|
|
start = int(index*1.15*RATE)
|
|
end = min(len(foliage),start+len(leaf))
|
|
foliage[start:end] += leaf[:end-start]*.30
|
|
foliage = save("foliage_loop",loop(eq(foliage,low=160,high=3300),1.5),["leaves/rustle/rustle01.flac","leaves/rustle/rustle04.flac","leaves/rustle/rustle09.flac"],"Layered leaf recordings with staggered overlaps, soft envelopes, filtering, crossfaded loop",True)
|
|
for index,leaf in enumerate(leaves,1):
|
|
save("leaves_%d"%index,fade(leaf,.1,.3),["leaves/rustle/rustle%02d.flac"%[1,4,9][index-1]],"Trimmed leaf rustle; softened attack and release")
|
|
wood = read("wood_creak.mp3")
|
|
for index,ratio in enumerate([.78,.94],1):
|
|
save("branch_creak_%d"%index,fade(eq(pitch(wood,ratio),low=120,high=3400),.04,.3),["wood_creak.mp3"],"Real wood flex/creak; mild pitch variation; no falling-tree impact")
|
|
for index,offset in enumerate([3,15,29],1):
|
|
save("bird_%d"%index,fade(eq(read("birds.ogg",offset,5),low=650,high=7000),.5,1),["birds.ogg: %d..%d seconds"%(offset,offset+5)],"Natural distant birds, cut into independent calls; filtered noise; soft boundaries")
|
|
cricket = next((SOURCE/"fauna").rglob("amb_cricket_1.flac"))
|
|
frog = next((SOURCE/"fauna").rglob("amb_frog_1.flac"))
|
|
save("insects",fade(eq(read(cricket),low=900,high=7500),.08,.25),[str(cricket.relative_to(SOURCE))],"Cricket recording, gentle filtering and envelope")
|
|
save("marsh_call",fade(eq(pitch(read(frog),.92),low=180,high=4600),.05,.25),[str(frog.relative_to(SOURCE))],"Frog recording, slightly lower pitch, restrained high frequencies")
|
|
(OUT/"manifest.json").write_text(json.dumps(manifest,indent=2),encoding="utf-8")
|
|
preview = np.zeros((int(32*RATE),2))
|
|
for bed,level in [(wind,-17),(foliage,-25)]: preview += bed[np.arange(len(preview))%len(bed)]*10**(level/20)
|
|
for name,offset,level in [("bird_1",2,-13),("leaves_1",7,-14),("branch_creak_1",12,-14),("wind_gust",16,-20),("marsh_call",22,-16),("insects",27,-19)]:
|
|
clip,_ = sf.read(OUT/(name+".wav"),always_2d=True)
|
|
at = int(offset*RATE)
|
|
end = min(len(preview),at+len(clip))
|
|
preview[at:end] += clip[:end-at]*10**(level/20)
|
|
preview = fade(peak(preview,-3),1,1.5)
|
|
sf.write(ROOT/"artifacts/planet_ambience_demo.wav",preview,RATE,subtype="PCM_16")
|
|
print(json.dumps(manifest,indent=2))
|
|
|
|
if __name__ == "__main__": main()
|