diff --git a/rsbuild.config.ts b/rsbuild.config.ts
index 933a6d027..cec3d9010 100644
--- a/rsbuild.config.ts
+++ b/rsbuild.config.ts
@@ -103,6 +103,9 @@ const appConfig = defineConfig({
                         configJson.defaultProxy = ':8080'
                     }
                     fs.writeFileSync('./dist/config.json', JSON.stringify({ ...configJson, ...configLocalJson }), 'utf8')
+                    if (fs.existsSync('./generated/sounds.js')) {
+                        fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
+                    }
                     // childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
                     // childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
                     // childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
diff --git a/scripts/downloadSoundsMap.mjs b/scripts/downloadSoundsMap.mjs
index 3c335f8f2..1e56131c2 100644
--- a/scripts/downloadSoundsMap.mjs
+++ b/scripts/downloadSoundsMap.mjs
@@ -1,6 +1,6 @@
 import fs from 'fs'
 
-const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds.js'
+const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds-v2.js'
 const savePath = 'dist/sounds.js'
 fetch(url).then(res => res.text()).then(data => {
   fs.writeFileSync(savePath, data, 'utf8')
diff --git a/scripts/prepareSounds.mjs b/scripts/prepareSounds.mjs
index 8f3e5bef9..f9b8cd609 100644
--- a/scripts/prepareSounds.mjs
+++ b/scripts/prepareSounds.mjs
@@ -10,26 +10,31 @@ import { build } from 'esbuild'
 
 const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
 
-const targetedVersions = ['1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
+const targetedVersions = ['1.21.1', '1.20.6', '1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
 
 /** @type {{name, size, hash}[]} */
 let prevSounds = null
 
 const burgerDataUrl = (version) => `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/${version}.json`
 const burgerDataPath = './generated/burger.json'
+const EXISTING_CACHE_PATH = './generated/existing-sounds-cache.json'
 
 // const perVersionData: Record<string, { removed: string[],
 
 const soundsPathVersionsRemap = {}
 
-const downloadAllSounds = async () => {
+const downloadAllSoundsAndCreateMap = async () => {
+  let existingSoundsCache = {}
+  try {
+    existingSoundsCache = JSON.parse(await fs.promises.readFile(EXISTING_CACHE_PATH, 'utf8'))
+  } catch (err) {}
   const { versions } = await getVersionList()
   const lastVersion = versions.filter(version => !version.id.includes('w'))[0]
   // if (lastVersion.id !== targetedVersions[0]) throw new Error('last version is not the same as targetedVersions[0], update')
-  for (const targetedVersion of targetedVersions) {
-    const versionData = versions.find(x => x.id === targetedVersion)
-    if (!versionData) throw new Error('no version data for ' + targetedVersion)
-    console.log('Getting assets for version', targetedVersion)
+  for (const version of targetedVersions) {
+    const versionData = versions.find(x => x.id === version)
+    if (!versionData) throw new Error('no version data for ' + version)
+    console.log('Getting assets for version', version)
     const { assetIndex } = await fetch(versionData.url).then((r) => r.json())
     /** @type {{objects: {[a: string]: { size, hash }}}} */
     const index = await fetch(assetIndex.url).then((r) => r.json())
@@ -45,26 +50,30 @@ const downloadAllSounds = async () => {
       const changedSize = soundAssets.filter(x => prevSoundNames.has(x.name) && prevSounds.find(y => y.name === x.name).size !== x.size)
       console.log('changed size', changedSize.map(x => ({ name: x.name, prev: prevSounds.find(y => y.name === x.name).size, curr: x.size })))
       if (addedSounds.length || changedSize.length) {
-        soundsPathVersionsRemap[targetedVersion] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
+        soundsPathVersionsRemap[version] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
       }
       if (addedSounds.length) {
-        console.log('downloading new sounds for version', targetedVersion)
-        downloadSounds(addedSounds, targetedVersion + '/')
+        console.log('downloading new sounds for version', version)
+        downloadSounds(version, addedSounds, version + '/')
       }
       if (changedSize.length) {
-        console.log('downloading changed sounds for version', targetedVersion)
-        downloadSounds(changedSize, targetedVersion + '/')
+        console.log('downloading changed sounds for version', version)
+        downloadSounds(version, changedSize, version + '/')
       }
     } else {
-      console.log('downloading sounds for version', targetedVersion)
-      downloadSounds(soundAssets)
+      console.log('downloading sounds for version', version)
+      downloadSounds(version, soundAssets)
     }
     prevSounds = soundAssets
   }
   async function downloadSound({ name, hash, size }, namePath, log) {
+    const cached =
+      !!namePath.replace('.ogg', '.mp3').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds) ||
+      !!namePath.replace('.ogg', '.ogg').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds)
     const savePath = path.resolve(`generated/sounds/${namePath}`)
-    if (fs.existsSync(savePath)) {
+    if (cached || fs.existsSync(savePath)) {
       // console.log('skipped', name)
+      existingSoundsCache.sounds[namePath] = true
       return
     }
     log()
@@ -86,7 +95,12 @@ const downloadAllSounds = async () => {
     }
     writer.close()
   }
-  async function downloadSounds(assets, addPath = '') {
+  async function downloadSounds(version, assets, addPath = '') {
+    if (addPath && existingSoundsCache.sounds[version]) {
+      console.log('using existing sounds for version', version)
+      return
+    }
+    console.log(version, 'have to download', assets.length, 'sounds')
     for (let i = 0; i < assets.length; i += 5) {
       await Promise.all(assets.slice(i, i + 5).map((asset, j) => downloadSound(asset, `${addPath}${asset.name}`, () => {
         console.log('downloading', addPath, asset.name, i + j, '/', assets.length)
@@ -95,6 +109,7 @@ const downloadAllSounds = async () => {
   }
 
   fs.writeFileSync('./generated/soundsPathVersionsRemap.json', JSON.stringify(soundsPathVersionsRemap), 'utf8')
+  fs.writeFileSync(EXISTING_CACHE_PATH, JSON.stringify(existingSoundsCache), 'utf8')
 }
 
 const lightpackOverrideSounds = {
@@ -106,7 +121,8 @@ const lightpackOverrideSounds = {
 // this is not done yet, will be used to select only sounds for bundle (most important ones)
 const isSoundWhitelisted = (name) => name.startsWith('random/') || name.startsWith('note/') || name.endsWith('/say1') || name.endsWith('/death') || (name.startsWith('mob/') && name.endsWith('/step1')) || name.endsWith('/swoop1') || /* name.endsWith('/break1') || */ name.endsWith('dig/stone1')
 
-const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // will be ffmpeg-static
+// const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // can be ffmpeg-static
+const ffmpegExec = 'ffmpeg'
 const maintainBitrate = true
 
 const scanFilesDeep = async (root, onOggFile) => {
@@ -127,7 +143,7 @@ const convertSounds = async () => {
   })
 
   const convertSound = async (i) => {
-    const proc = promisify(exec)(`${ffmpeg} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
+    const proc = promisify(exec)(`${ffmpegExec} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
     // pipe stdout to the console
     proc.child.stdout.pipe(process.stdout)
     await proc
@@ -147,8 +163,8 @@ const getSoundsMap = (burgerData) => {
 }
 
 const writeSoundsMap = async () => {
-  // const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
-  // fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
+  const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
+  fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
 
   const allSoundsMapOutput = {}
   let prevMap
@@ -174,16 +190,22 @@ const writeSoundsMap = async () => {
       // const includeSound = isSoundWhitelisted(firstName)
       // if (!includeSound) continue
       const mostUsedSound = sounds.sort((a, b) => b.weight - a.weight)[0]
-      const targetSound = sounds[0]
       // outputMap[id] = { subtitle, sounds: mostUsedSound }
       // outputMap[id] = { subtitle, sounds }
-      const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
+      // const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
       // if (!fs.existsSync(soundFilePath)) {
       //   console.warn('no sound file', targetSound.name)
       //   continue
       // }
+      let outputUseSoundLine = []
+      const minWeight = sounds.reduce((acc, cur) => cur.weight ? Math.min(acc, cur.weight) : acc, sounds[0].weight ?? 1)
+      if (isNaN(minWeight)) debugger
+      for (const sound of sounds) {
+        if (sound.weight && isNaN(sound.weight)) debugger
+        outputUseSoundLine.push(`${sound.volume ?? 1};${sound.name};${sound.weight ?? minWeight}`)
+      }
       const key = `${id};${name}`
-      outputIdMap[key] = `${targetSound.volume ?? 1};${targetSound.name}`
+      outputIdMap[key] = outputUseSoundLine.join(',')
       if (prevMap && prevMap[key]) {
         keysStats.same++
       } else {
@@ -221,7 +243,7 @@ const makeSoundsBundle = async () => {
 
   const allSoundsMeta = {
     format: 'mp3',
-    baseUrl: 'https://raw.githubusercontent.com/zardoy/minecraft-web-client/sounds-generated/sounds/'
+    baseUrl: `https://raw.githubusercontent.com/${process.env.REPO_SLUG}/sounds/sounds/`
   }
 
   await build({
@@ -235,9 +257,25 @@ const makeSoundsBundle = async () => {
     },
     metafile: true,
   })
+  // copy also to generated/sounds.js
+  fs.copyFileSync('./dist/sounds.js', './generated/sounds.js')
 }
 
-// downloadAllSounds()
-// convertSounds()
-// writeSoundsMap()
-// makeSoundsBundle()
+const action = process.argv[2]
+if (action) {
+  const execFn = {
+    download: downloadAllSoundsAndCreateMap,
+    convert: convertSounds,
+    write: writeSoundsMap,
+    bundle: makeSoundsBundle,
+  }[action]
+
+  if (execFn) {
+    execFn()
+  }
+} else {
+  // downloadAllSoundsAndCreateMap()
+  // convertSounds()
+  // writeSoundsMap()
+  makeSoundsBundle()
+}
diff --git a/scripts/uploadSoundFiles.ts b/scripts/uploadSoundFiles.ts
new file mode 100644
index 000000000..e8677c872
--- /dev/null
+++ b/scripts/uploadSoundFiles.ts
@@ -0,0 +1,109 @@
+import fetch from 'node-fetch';
+import * as fs from 'fs';
+import * as path from 'path';
+import { glob } from 'glob';
+
+// Git details
+const REPO_SLUG = process.env.REPO_SLUG;
+const owner = REPO_SLUG.split('/')[0];
+const repo = REPO_SLUG.split('/')[1];
+const branch = "sounds";
+
+// GitHub token for authentication
+const token = process.env.GITHUB_TOKEN;
+
+// GitHub API endpoint
+const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
+
+const headers = {
+    Authorization: `token ${token}`,
+    'Content-Type': 'application/json'
+};
+
+async function getShaForExistingFile(repoFilePath: string): Promise<string | null> {
+    const url = `${baseUrl}/${repoFilePath}?ref=${branch}`;
+    const response = await fetch(url, { headers });
+    if (response.status === 404) {
+        return null; // File does not exist
+    }
+    if (!response.ok) {
+        throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
+    }
+    const data = await response.json();
+    return data.sha;
+}
+
+async function uploadFiles() {
+    const commitMessage = "Upload multiple files via script";
+    const committer = {
+        name: "GitHub",
+        email: "noreply@github.com"
+    };
+
+    const filesToUpload = glob.sync("generated/sounds/**/*.mp3").map(localPath => {
+        const repoPath = localPath.replace(/^generated\//, '');
+        return { localPath, repoPath };
+    });
+
+    const files = await Promise.all(filesToUpload.map(async file => {
+        const content = fs.readFileSync(file.localPath, 'base64');
+        const sha = await getShaForExistingFile(file.repoPath);
+        return {
+            path: file.repoPath,
+            mode: "100644",
+            type: "blob",
+            sha: sha || undefined,
+            content: content
+        };
+    }));
+
+    const treeResponse = await fetch(`${baseUrl}/git/trees`, {
+        method: 'POST',
+        headers: headers,
+        body: JSON.stringify({
+            base_tree: null,
+            tree: files
+        })
+    });
+
+    if (!treeResponse.ok) {
+        throw new Error(`Failed to create tree: ${treeResponse.statusText}`);
+    }
+
+    const treeData = await treeResponse.json();
+
+    const commitResponse = await fetch(`${baseUrl}/git/commits`, {
+        method: 'POST',
+        headers: headers,
+        body: JSON.stringify({
+            message: commitMessage,
+            tree: treeData.sha,
+            parents: [branch],
+            committer: committer
+        })
+    });
+
+    if (!commitResponse.ok) {
+        throw new Error(`Failed to create commit: ${commitResponse.statusText}`);
+    }
+
+    const commitData = await commitResponse.json();
+
+    const updateRefResponse = await fetch(`${baseUrl}/git/refs/heads/${branch}`, {
+        method: 'PATCH',
+        headers: headers,
+        body: JSON.stringify({
+            sha: commitData.sha
+        })
+    });
+
+    if (!updateRefResponse.ok) {
+        throw new Error(`Failed to update ref: ${updateRefResponse.statusText}`);
+    }
+
+    console.log("Files uploaded successfully");
+}
+
+uploadFiles().catch(error => {
+    console.error("Error uploading files:", error);
+});
diff --git a/scripts/uploadSounds.ts b/scripts/uploadSounds.ts
new file mode 100644
index 000000000..b0e9ecd79
--- /dev/null
+++ b/scripts/uploadSounds.ts
@@ -0,0 +1,67 @@
+import fs from 'fs'
+
+// GitHub details
+const owner = "zardoy";
+const repo = "minecraft-web-client";
+const branch = "sounds-generated";
+const filePath = "dist/sounds.js"; // Local file path
+const repoFilePath = "sounds-v2.js"; // Path in the repo
+
+// GitHub token for authentication
+const token = process.env.GITHUB_TOKEN;
+
+// GitHub API endpoint
+const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${repoFilePath}`;
+
+const headers = {
+    Authorization: `token ${token}`,
+    'Content-Type': 'application/json'
+};
+
+async function getShaForExistingFile(): Promise<string | null> {
+    const url = `${baseUrl}?ref=${branch}`;
+    const response = await fetch(url, { headers });
+    if (response.status === 404) {
+        return null; // File does not exist
+    }
+    if (!response.ok) {
+        throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
+    }
+    const data = await response.json();
+    return data.sha;
+}
+
+async function uploadFile() {
+    const content = fs.readFileSync(filePath, 'utf8');
+    const base64Content = Buffer.from(content).toString('base64');
+    const sha = await getShaForExistingFile();
+    console.log('got sha')
+
+    const body = {
+        message: "Update sounds.js",
+        content: base64Content,
+        branch: branch,
+        committer: {
+            name: "GitHub",
+            email: "noreply@github.com"
+        },
+        sha: sha || undefined
+    };
+
+    const response = await fetch(baseUrl, {
+        method: 'PUT',
+        headers: headers,
+        body: JSON.stringify(body)
+    });
+
+    if (!response.ok) {
+        throw new Error(`Failed to upload file: ${response.statusText}`);
+    }
+
+    const responseData = await response.json();
+    console.log("File uploaded successfully:", responseData);
+}
+
+uploadFile().catch(error => {
+    console.error("Error uploading file:", error);
+});
diff --git a/src/resourcePack.ts b/src/resourcePack.ts
index d7bbdc47d..124d7e6cc 100644
--- a/src/resourcePack.ts
+++ b/src/resourcePack.ts
@@ -157,7 +157,7 @@ const getSizeFromImage = async (filePath: string) => {
   return probeImg.width
 }
 
-export const getActiveTexturepackBasePath = async () => {
+export const getActiveResourcepackBasePath = async () => {
   if (await existsAsync('/resourcepack/pack.mcmeta')) {
     return '/resourcepack'
   }
@@ -198,7 +198,7 @@ const getFilesMapFromDir = async (dir: string) => {
 }
 
 export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
-  const basePath = await getActiveTexturepackBasePath()
+  const basePath = await getActiveResourcepackBasePath()
   if (!basePath) return
   let firstTextureSize: number | undefined
   const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
@@ -282,7 +282,7 @@ const prepareBlockstatesAndModels = async () => {
   viewer.world.customBlockStates = {}
   viewer.world.customModels = {}
   const usedTextures = new Set<string>()
-  const basePath = await getActiveTexturepackBasePath()
+  const basePath = await getActiveResourcepackBasePath()
   if (!basePath) return
   if (appStatusState.status) {
     setLoadingScreenStatus('Reading resource pack blockstates and models')
@@ -361,6 +361,7 @@ export const onAppLoad = () => {
   customEvents.on('mineflayerBotCreated', () => {
     // todo also handle resourcePack
     const handleResourcePackRequest = async (packet) => {
+      console.log('Received resource pack request', packet)
       if (options.serverResourcePacks === 'never') return
       const promptMessagePacket = ('promptMessage' in packet && packet.promptMessage) ? packet.promptMessage : undefined
       const promptMessageText = promptMessagePacket ? '' : 'Do you want to use server resource pack?'
@@ -397,7 +398,7 @@ export const onAppLoad = () => {
 }
 
 const updateAllReplacableTextures = async () => {
-  const basePath = await getActiveTexturepackBasePath()
+  const basePath = await getActiveResourcepackBasePath()
   const setCustomCss = async (path: string | null, varName: string, repeat = 1) => {
     if (path && await existsAsync(path)) {
       const contents = await fs.promises.readFile(path, 'base64')
diff --git a/src/soundSystem.ts b/src/soundSystem.ts
index d0caf01f9..841644cc5 100644
--- a/src/soundSystem.ts
+++ b/src/soundSystem.ts
@@ -1,50 +1,45 @@
 import { subscribeKey } from 'valtio/utils'
 import { Vec3 } from 'vec3'
-import { versionToMajor, versionToNumber, versionsMapToMajor } from 'prismarine-viewer/viewer/prepare/utils'
+import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
 import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
 import type { Block } from 'prismarine-block'
 import { miscUiState } from './globalState'
 import { options } from './optionsStorage'
 import { loadOrPlaySound } from './basicSounds'
-import { showNotification } from './react/NotificationProvider'
+import { createSoundMap, SoundMap } from './soundsMap'
+import { getActiveResourcepackBasePath, resourcePackState } from './resourcePack'
 
-const globalObject = window as {
-  allSoundsMap?: Record<string, Record<string, string>>,
-  allSoundsVersionedMap?: Record<string, string[]>,
+let soundMap: SoundMap | undefined
+
+const updateResourcePack = async () => {
+  if (!soundMap) return
+  soundMap.activeResourcePackBasePath = await getActiveResourcepackBasePath() ?? undefined
 }
 
 subscribeKey(miscUiState, 'gameLoaded', async () => {
-  if (!miscUiState.gameLoaded) return
-  const soundsLegacyMap = window.allSoundsVersionedMap as Record<string, string[]>
-  const { allSoundsMap } = globalObject
-  const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string }
-  if (!allSoundsMap) {
-    return
-  }
-
-  const allSoundsMajor = versionsMapToMajor(allSoundsMap)
-  const soundsMap = allSoundsMajor[versionToMajor(bot.version)] ?? Object.values(allSoundsMajor)[0]
-
-  if (!soundsMap || !miscUiState.gameLoaded || !loadedData.sounds) {
+  if (!miscUiState.gameLoaded || !loadedData.sounds) {
     return
   }
 
-  // const soundsPerId = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [+id.split(';')[0], sound]))
-  const soundsPerName = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [id.split(';')[1], sound]))
+  console.log(`Loading sounds for version ${bot.version}. Resourcepack state: ${JSON.stringify(resourcePackState)}`)
+  soundMap = createSoundMap(bot.version) ?? undefined
+  if (!soundMap) return
+  void updateResourcePack()
 
   const playGeneralSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
-    if (!options.volume) return
-    const soundStaticData = soundsPerName[soundKey]?.split(';')
-    if (!soundStaticData) return
-    const soundVolume = +soundStaticData[0]!
-    const soundPath = soundStaticData[1]!
-    const versionedSound = getVersionedSound(bot.version, soundPath, Object.entries(soundsLegacyMap))
-    // todo test versionedSound
-    const url = allSoundsMeta.baseUrl.replace(/\/$/, '') + (versionedSound ? `/${versionedSound}` : '') + '/minecraft/sounds/' + soundPath + '.' + allSoundsMeta.format
-    const isMuted = options.mutedSounds.includes(soundKey) || options.mutedSounds.includes(soundPath) || options.volume === 0
+    if (!options.volume || !soundMap) return
+    const soundData = await soundMap.getSoundUrl(soundKey, volume)
+    if (!soundData) return
+
+    const isMuted = options.mutedSounds.includes(soundKey) || options.volume === 0
     if (position) {
       if (!isMuted) {
-        viewer.playSound(position, url, soundVolume * Math.max(Math.min(volume, 1), 0) * (options.volume / 100), Math.max(Math.min(pitch ?? 1, 2), 0.5))
+        viewer.playSound(
+          position,
+          soundData.url,
+          soundData.volume * (options.volume / 100),
+          Math.max(Math.min(pitch ?? 1, 2), 0.5)
+        )
       }
       if (getDistance(bot.entity.position, position) < 4 * 16) {
         lastPlayedSounds.lastServerPlayed[soundKey] ??= { count: 0, last: 0 }
@@ -53,7 +48,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
       }
     } else {
       if (!isMuted) {
-        await loadOrPlaySound(url, volume)
+        await loadOrPlaySound(soundData.url, volume)
       }
       lastPlayedSounds.lastClientPlayed.push(soundKey)
       if (lastPlayedSounds.lastClientPlayed.length > 10) {
@@ -61,84 +56,38 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
       }
     }
   }
+
   const playHardcodedSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
     await playGeneralSound(soundKey, position, volume, pitch)
   }
+
   bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
     await playHardcodedSound(soundId, position, volume, pitch)
   })
+
   bot.on('hardcodedSoundEffectHeard', async (soundIdNum, soundCategory, position, volume, pitch) => {
     const fixOffset = versionToNumber('1.20.4') === versionToNumber(bot.version) ? -1 : 0
     const soundKey = loadedData.sounds[soundIdNum + fixOffset]?.name
     if (soundKey === undefined) return
     await playGeneralSound(soundKey, position, volume, pitch)
   })
-  // workaround as mineflayer doesn't support soundEvent
+
   bot._client.on('sound_effect', async (packet) => {
     const soundResource = packet['soundEvent']?.resource as string | undefined
     if (packet.soundId !== 0 || !soundResource) return
     const pos = new Vec3(packet.x / 8, packet.y / 8, packet.z / 8)
     await playHardcodedSound(soundResource.replace('minecraft:', ''), pos, packet.volume, packet.pitch)
   })
+
   bot.on('entityHurt', async (entity) => {
     if (entity.id === bot.entity.id) {
       await playHardcodedSound('entity.player.hurt')
     }
   })
 
-  const useBlockSound = (blockName: string, category: string, fallback: string) => {
-    blockName = {
-      // todo somehow generated, not full
-      grass_block: 'grass',
-      tall_grass: 'grass',
-      fern: 'grass',
-      large_fern: 'grass',
-      dead_bush: 'grass',
-      seagrass: 'grass',
-      tall_seagrass: 'grass',
-      kelp: 'grass',
-      kelp_plant: 'grass',
-      sugar_cane: 'grass',
-      bamboo: 'grass',
-      vine: 'grass',
-      nether_sprouts: 'grass',
-      nether_wart: 'grass',
-      twisting_vines: 'grass',
-      weeping_vines: 'grass',
-
-      cobblestone: 'stone',
-      stone_bricks: 'stone',
-      mossy_stone_bricks: 'stone',
-      cracked_stone_bricks: 'stone',
-      chiseled_stone_bricks: 'stone',
-      stone_brick_slab: 'stone',
-      stone_brick_stairs: 'stone',
-      stone_brick_wall: 'stone',
-      polished_granite: 'stone',
-    }[blockName] ?? blockName
-    const key = 'block.' + blockName + '.' + category
-    return soundsPerName[key] ? key : fallback
-  }
-
-  const getStepSound = (blockUnder: Block) => {
-    // const soundsMap = globalObject.allSoundsMap?.[bot.version]
-    // if (!soundsMap) return
-    // let soundResult = 'block.stone.step'
-    // for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) {
-    //   const match = /block\.(.+)\.step/.exec(x)
-    //   const block = match?.[1]
-    //   if (!block) continue
-    //   if (loadedData.blocksByName[block]?.name === blockUnder.name) {
-    //     soundResult = x
-    //     break
-    //   }
-    // }
-    return useBlockSound(blockUnder.name, 'step', 'block.stone.step')
-  }
-
   let lastStepSound = 0
   const movementHappening = async () => {
-    if (!bot.player) return // no info yet
+    if (!bot.player || !soundMap) return // no info yet
     const VELOCITY_THRESHOLD = 0.1
     const { x, z, y } = bot.player.entity.velocity
     if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) {
@@ -146,9 +95,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
       if (Date.now() - lastStepSound > 300) {
         const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0))
         if (blockUnder) {
-          const stepSound = getStepSound(blockUnder)
+          const stepSound = soundMap.getStepSound(blockUnder.name)
           if (stepSound) {
-            await playHardcodedSound(stepSound, undefined, 0.6)// todo not sure why 0.6
+            await playHardcodedSound(stepSound, undefined, 0.6)
             lastStepSound = Date.now()
           }
         }
@@ -157,8 +106,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
   }
 
   const playBlockBreak = async (blockName: string, position?: Vec3) => {
-    const sound = useBlockSound(blockName, 'break', 'block.stone.break')
-
+    if (!soundMap) return
+    const sound = soundMap.getBreakSound(blockName)
     await playHardcodedSound(sound, position, 0.6, 1)
   }
 
@@ -200,8 +149,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
       if (effectId === 1010) {
         console.log('play record', data)
       }
-      // todo add support for all current world events
     })
+
     let diggingBlock: Block | null = null
     customEvents.on('digStart', () => {
       diggingBlock = bot.blockAtCursor(5)
@@ -214,40 +163,14 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
   }
 
   registerEvents()
-
-  // 1.20+ soundEffectHeard is broken atm
-  // bot._client.on('packet', (data, { name }, buffer) => {
-  //   if (name === 'sound_effect') {
-  //     console.log(data, buffer)
-  //   }
-  // })
 })
 
-// todo
-// const music = {
-//   activated: false,
-//   playing: '',
-//   activate () {
-//     const gameMusic = Object.entries(globalObject.allSoundsMap?.[bot.version] ?? {}).find(([id, sound]) => sound.includes('music.game'))
-//     if (!gameMusic) return
-//     const soundPath = gameMusic[0].split(';')[1]
-//     const next = () => {}
-//   }
-// }
-
-const getVersionedSound = (version: string, item: string, itemsMapSortedEntries: Array<[string, string[]]>) => {
-  const verNumber = versionToNumber(version)
-  for (const [itemsVer, items] of itemsMapSortedEntries) {
-    // 1.18 < 1.18.1
-    // 1.13 < 1.13.2
-    if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
-      return itemsVer
-    }
-  }
-}
+subscribeKey(resourcePackState, 'resourcePackInstalled', async () => {
+  await updateResourcePack()
+})
 
 export const downloadSoundsIfNeeded = async () => {
-  if (!globalObject.allSoundsMap) {
+  if (!window.allSoundsMap) {
     try {
       await loadScript('./sounds.js')
     } catch (err) {
diff --git a/src/sounds/testSounds.ts b/src/sounds/testSounds.ts
new file mode 100644
index 000000000..6fdaecf65
--- /dev/null
+++ b/src/sounds/testSounds.ts
@@ -0,0 +1,8 @@
+import { createSoundMap } from '../soundsMap'
+
+//@ts-expect-error
+globalThis.window = {}
+require('../../generated/sounds.js')
+
+const soundMap = createSoundMap('1.20.1')
+console.log(soundMap?.getSoundUrl('ambient.cave'))
diff --git a/src/soundsMap.ts b/src/soundsMap.ts
new file mode 100644
index 000000000..7bad1b3b6
--- /dev/null
+++ b/src/soundsMap.ts
@@ -0,0 +1,339 @@
+import fs from 'fs'
+import path from 'path'
+import { versionsMapToMajor, versionToMajor, versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
+
+interface SoundMeta {
+  format: string
+  baseUrl: string
+}
+
+interface SoundData {
+  volume: number
+  path: string
+}
+
+interface SoundMapData {
+  allSoundsMap: Record<string, Record<string, string>>
+  soundsLegacyMap: Record<string, string[]>
+  soundsMeta: SoundMeta
+}
+
+interface BlockSoundMap {
+  [blockName: string]: string
+}
+
+interface SoundEntry {
+  file: string
+  weight: number
+  volume: number
+}
+
+export class SoundMap {
+  private readonly soundsPerName: Record<string, SoundEntry[]>
+  private readonly existingResourcePackPaths: Set<string>
+  public activeResourcePackBasePath: string | undefined
+
+  constructor (
+    private readonly soundData: SoundMapData,
+    private readonly version: string
+  ) {
+    const allSoundsMajor = versionsMapToMajor(soundData.allSoundsMap)
+    const soundsMap = allSoundsMajor[versionToMajor(version)] ?? Object.values(allSoundsMajor)[0]
+    this.soundsPerName = Object.fromEntries(
+      Object.entries(soundsMap).map(([id, soundsStr]) => {
+        const sounds = soundsStr.split(',').map(s => {
+          const [volume, name, weight] = s.split(';')
+          if (isNaN(Number(volume))) throw new Error('volume is not a number')
+          if (isNaN(Number(weight))) {
+            // debugger
+            throw new TypeError('weight is not a number')
+          }
+          return {
+            file: name,
+            weight: Number(weight),
+            volume: Number(volume)
+          }
+        })
+        return [id.split(';')[1], sounds]
+      })
+    )
+  }
+
+  async updateExistingResourcePackPaths () {
+    if (!this.activeResourcePackBasePath) return
+    // todo support sounds.js from resource pack
+    const soundsBasePath = path.join(this.activeResourcePackBasePath, 'assets/minecraft/sounds')
+    // scan recursively for sounds files
+    const scan = async (dir: string) => {
+      const entries = await fs.promises.readdir(dir, { withFileTypes: true })
+      for (const entry of entries) {
+        const entryPath = path.join(dir, entry.name)
+        if (entry.isDirectory()) {
+          await scan(entryPath)
+        } else if (entry.isFile() && entry.name.endsWith('.ogg')) {
+          const relativePath = path.relative(soundsBasePath, entryPath)
+          this.existingResourcePackPaths.add(relativePath)
+        }
+      }
+    }
+
+    await scan(soundsBasePath)
+  }
+
+  async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> {
+    const sounds = this.soundsPerName[soundKey]
+    if (!sounds?.length) return undefined
+
+    // Pick a random sound based on weights
+    const totalWeight = sounds.reduce((sum, s) => sum + s.weight, 0)
+    let random = Math.random() * totalWeight
+    const sound = sounds.find(s => {
+      random -= s.weight
+      return random <= 0
+    }) ?? sounds[0]
+
+    const versionedSound = this.getVersionedSound(sound.file)
+
+    let url = this.soundData.soundsMeta.baseUrl.replace(/\/$/, '') +
+      (versionedSound ? `/${versionedSound}` : '') +
+      '/minecraft/sounds/' +
+      sound.file +
+      '.' +
+      this.soundData.soundsMeta.format
+
+    // Try loading from resource pack file first
+    if (this.activeResourcePackBasePath) {
+      const tryFormat = async (format: string) => {
+        try {
+          const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.file}.${format}`)
+          const fileData = await fs.promises.readFile(resourcePackPath)
+          url = `data:audio/${format};base64,${fileData.toString('base64')}`
+          return true
+        } catch (err) {
+        }
+      }
+      const success = await tryFormat(this.soundData.soundsMeta.format)
+      if (!success && this.soundData.soundsMeta.format !== 'ogg') {
+        await tryFormat('ogg')
+      }
+    }
+
+    return {
+      url,
+      volume: sound.volume * Math.max(Math.min(volume, 1), 0)
+    }
+  }
+
+  private getVersionedSound (item: string): string | undefined {
+    const verNumber = versionToNumber(this.version)
+    const entries = Object.entries(this.soundData.soundsLegacyMap)
+    for (const [itemsVer, items] of entries) {
+      if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
+        return itemsVer
+      }
+    }
+    return undefined
+  }
+
+  getBlockSound (blockName: string, category: string, fallback: string): string {
+    const mappedName = blockSoundAliases[blockName] ?? blockName
+    const key = `block.${mappedName}.${category}`
+    return this.soundsPerName[key] ? key : fallback
+  }
+
+  getStepSound (blockName: string): string {
+    return this.getBlockSound(blockName, 'step', 'block.stone.step')
+  }
+
+  getBreakSound (blockName: string): string {
+    return this.getBlockSound(blockName, 'break', 'block.stone.break')
+  }
+}
+
+export function createSoundMap (version: string): SoundMap | null {
+  const globalObject = window as {
+    allSoundsMap?: Record<string, Record<string, string>>,
+    allSoundsVersionedMap?: Record<string, string[]>,
+    allSoundsMeta?: { format: string, baseUrl: string }
+  }
+  if (!globalObject.allSoundsMap) return null
+  return new SoundMap({
+    allSoundsMap: globalObject.allSoundsMap,
+    soundsLegacyMap: globalObject.allSoundsVersionedMap ?? {},
+    soundsMeta: globalObject.allSoundsMeta!
+  }, version)
+}
+
+// Block name mappings for sound effects
+const blockSoundAliases: BlockSoundMap = {
+  // Grass-like blocks
+  grass_block: 'grass',
+  tall_grass: 'grass',
+  fern: 'grass',
+  large_fern: 'grass',
+  dead_bush: 'grass',
+  seagrass: 'grass',
+  tall_seagrass: 'grass',
+  kelp: 'grass',
+  kelp_plant: 'grass',
+  sugar_cane: 'grass',
+  bamboo: 'grass',
+  vine: 'grass',
+  nether_sprouts: 'grass',
+  nether_wart: 'grass',
+  twisting_vines: 'grass',
+  weeping_vines: 'grass',
+  sweet_berry_bush: 'grass',
+  glow_lichen: 'grass',
+  moss_carpet: 'grass',
+  moss_block: 'grass',
+  hanging_roots: 'grass',
+  spore_blossom: 'grass',
+  small_dripleaf: 'grass',
+  big_dripleaf: 'grass',
+  flowering_azalea: 'grass',
+  azalea: 'grass',
+  azalea_leaves: 'grass',
+  flowering_azalea_leaves: 'grass',
+
+  // Stone-like blocks
+  cobblestone: 'stone',
+  stone_bricks: 'stone',
+  mossy_stone_bricks: 'stone',
+  cracked_stone_bricks: 'stone',
+  chiseled_stone_bricks: 'stone',
+  stone_brick_slab: 'stone',
+  stone_brick_stairs: 'stone',
+  stone_brick_wall: 'stone',
+  polished_granite: 'stone',
+  granite: 'stone',
+  andesite: 'stone',
+  diorite: 'stone',
+  polished_andesite: 'stone',
+  polished_diorite: 'stone',
+  deepslate: 'deepslate',
+  cobbled_deepslate: 'deepslate',
+  polished_deepslate: 'deepslate',
+  deepslate_bricks: 'deepslate_bricks',
+  deepslate_tiles: 'deepslate_tiles',
+  calcite: 'stone',
+  tuff: 'stone',
+  smooth_stone: 'stone',
+  smooth_sandstone: 'stone',
+  smooth_quartz: 'stone',
+  smooth_red_sandstone: 'stone',
+
+  // Wood-like blocks
+  oak_planks: 'wood',
+  spruce_planks: 'wood',
+  birch_planks: 'wood',
+  jungle_planks: 'wood',
+  acacia_planks: 'wood',
+  dark_oak_planks: 'wood',
+  crimson_planks: 'wood',
+  warped_planks: 'wood',
+  oak_log: 'wood',
+  spruce_log: 'wood',
+  birch_log: 'wood',
+  jungle_log: 'wood',
+  acacia_log: 'wood',
+  dark_oak_log: 'wood',
+  crimson_stem: 'stem',
+  warped_stem: 'stem',
+
+  // Metal blocks
+  iron_block: 'metal',
+  gold_block: 'metal',
+  copper_block: 'copper',
+  exposed_copper: 'copper',
+  weathered_copper: 'copper',
+  oxidized_copper: 'copper',
+  netherite_block: 'netherite_block',
+  ancient_debris: 'ancient_debris',
+  lodestone: 'lodestone',
+  chain: 'chain',
+  anvil: 'anvil',
+  chipped_anvil: 'anvil',
+  damaged_anvil: 'anvil',
+
+  // Glass blocks
+  glass: 'glass',
+  glass_pane: 'glass',
+  white_stained_glass: 'glass',
+  orange_stained_glass: 'glass',
+  magenta_stained_glass: 'glass',
+  light_blue_stained_glass: 'glass',
+  yellow_stained_glass: 'glass',
+  lime_stained_glass: 'glass',
+  pink_stained_glass: 'glass',
+  gray_stained_glass: 'glass',
+  light_gray_stained_glass: 'glass',
+  cyan_stained_glass: 'glass',
+  purple_stained_glass: 'glass',
+  blue_stained_glass: 'glass',
+  brown_stained_glass: 'glass',
+  green_stained_glass: 'glass',
+  red_stained_glass: 'glass',
+  black_stained_glass: 'glass',
+  tinted_glass: 'glass',
+
+  // Wool blocks
+  white_wool: 'wool',
+  orange_wool: 'wool',
+  magenta_wool: 'wool',
+  light_blue_wool: 'wool',
+  yellow_wool: 'wool',
+  lime_wool: 'wool',
+  pink_wool: 'wool',
+  gray_wool: 'wool',
+  light_gray_wool: 'wool',
+  cyan_wool: 'wool',
+  purple_wool: 'wool',
+  blue_wool: 'wool',
+  brown_wool: 'wool',
+  green_wool: 'wool',
+  red_wool: 'wool',
+  black_wool: 'wool',
+
+  // Nether blocks
+  netherrack: 'netherrack',
+  nether_bricks: 'nether_bricks',
+  red_nether_bricks: 'nether_bricks',
+  nether_wart_block: 'wart_block',
+  warped_wart_block: 'wart_block',
+  soul_sand: 'soul_sand',
+  soul_soil: 'soul_soil',
+  basalt: 'basalt',
+  polished_basalt: 'basalt',
+  blackstone: 'gilded_blackstone',
+  gilded_blackstone: 'gilded_blackstone',
+
+  // Amethyst blocks
+  amethyst_block: 'amethyst_block',
+  amethyst_cluster: 'amethyst_cluster',
+  large_amethyst_bud: 'large_amethyst_bud',
+  medium_amethyst_bud: 'medium_amethyst_bud',
+  small_amethyst_bud: 'small_amethyst_bud',
+
+  // Miscellaneous
+  sand: 'sand',
+  red_sand: 'sand',
+  gravel: 'gravel',
+  snow: 'snow',
+  snow_block: 'snow',
+  powder_snow: 'powder_snow',
+  ice: 'glass',
+  packed_ice: 'glass',
+  blue_ice: 'glass',
+  slime_block: 'slime_block',
+  honey_block: 'honey_block',
+  scaffolding: 'scaffolding',
+  ladder: 'ladder',
+  lantern: 'lantern',
+  soul_lantern: 'lantern',
+  pointed_dripstone: 'pointed_dripstone',
+  dripstone_block: 'dripstone_block',
+  rooted_dirt: 'rooted_dirt',
+  sculk_sensor: 'sculk_sensor',
+  shroomlight: 'shroomlight'
+}