Skip to content

Commit

Permalink
Audio Player: Add playback support for HZD MusicResource
Browse files Browse the repository at this point in the history
  • Loading branch information
ShadelessFox committed Aug 4, 2024
1 parent 67efb4e commit 9cabf8d
Show file tree
Hide file tree
Showing 24 changed files with 421 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
@Selector(type = @Type(name = "WwiseBankResource"), game = {GameType.DS, GameType.DSDC}),
@Selector(type = @Type(name = "WwiseWemResource"), game = {GameType.DS, GameType.DSDC}),
@Selector(type = @Type(name = "LocalizedSimpleSoundResource")),
@Selector(type = @Type(name = "WaveResource"), game = GameType.HZD)
@Selector(type = @Type(name = "WaveResource"), game = GameType.HZD),
@Selector(type = @Type(name = "MusicResource"), game = GameType.HZD)
})
public class AudioPlayer implements ValueViewer {
@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.shade.decima.ui.data.viewer.audio.playlists.ds.WwiseWemLocalizedPlaylist;
import com.shade.decima.ui.data.viewer.audio.playlists.ds.WwiseWemPlaylist;
import com.shade.decima.ui.data.viewer.audio.playlists.hzd.HZDLocalizedSoundPlaylist;
import com.shade.decima.ui.data.viewer.audio.playlists.hzd.MusicPlaylist;
import com.shade.decima.ui.data.viewer.audio.playlists.hzd.WavePlaylist;
import com.shade.decima.ui.data.viewer.audio.settings.AudioPlayerSettings;
import com.shade.decima.ui.menu.MenuConstants;
Expand Down Expand Up @@ -99,6 +100,7 @@ public void setInput(@NotNull Project project, @NotNull RTTIObject object) {
case "WwiseWemLocalizedResource" -> new WwiseWemLocalizedPlaylist(object);
case "LocalizedSimpleSoundResource" -> type == GameType.HZD ? new HZDLocalizedSoundPlaylist(object) : new DSLocalizedSoundPlaylist(object);
case "WaveResource" -> new WavePlaylist(object);
case "MusicResource" -> new MusicPlaylist(object);
default -> throw new IllegalArgumentException("Unsupported type: " + object.type().getTypeName());
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,19 @@ public static void extractTrack(

final Codec codec = playlist.getCodec(index);

if (codec instanceof Codec.Wave wave) {
extractFromWave(task.split(1), data, output, wave.encoding());
if (codec instanceof Codec.Generic generic) {
extractFromGeneric(task.split(1), data, output, generic.name());
} else {
extractFromWwise(task.split(1), data, output);
}
}
}

private static void extractFromWwise(@NotNull ProgressMonitor monitor, @NotNull byte[] data, @NotNull Path output) throws IOException, InterruptedException {
private static void extractFromWwise(
@NotNull ProgressMonitor monitor,
@NotNull byte[] data,
@NotNull Path output
) throws IOException, InterruptedException {
final AudioPlayerSettings settings = AudioPlayerSettings.getInstance();
final Path wemPath = Files.createTempFile(null, ".wem");
final Path oggPath = Path.of(IOUtils.getBasename(wemPath) + ".ogg");
Expand All @@ -82,23 +86,38 @@ private static void extractFromWwise(@NotNull ProgressMonitor monitor, @NotNull
}
}

private static void extractFromWave(@NotNull ProgressMonitor monitor, @NotNull byte[] data, @NotNull Path output, @NotNull String encoding) throws IOException, InterruptedException {
final Path wavPath = Files.createTempFile(null, ".wav");

try (var task = monitor.begin("Read wave audio", 1)) {
Files.write(wavPath, data);
private static void extractFromGeneric(
@NotNull ProgressMonitor monitor,
@NotNull byte[] data,
@NotNull Path output,
@NotNull String codec
) throws IOException, InterruptedException {
Path path = Files.createTempFile(null, ".bin");

convertAudio(task.split(1), wavPath, output, encoding);
try (var task = monitor.begin("Read " + codec + " audio", 1)) {
Files.write(path, data);
convertAudio(task.split(1), path, output, codec);
} finally {
Files.deleteIfExists(wavPath);
Files.deleteIfExists(path);
}
}

private static void convertAudio(@NotNull ProgressMonitor monitor, @NotNull Path input, @NotNull Path output, @NotNull String codec) throws IOException, InterruptedException {
final AudioPlayerSettings settings = AudioPlayerSettings.getInstance();
private static void convertAudio(
@NotNull ProgressMonitor monitor,
@NotNull Path input,
@NotNull Path output,
@NotNull String codec
) throws IOException, InterruptedException {
AudioPlayerSettings settings = AudioPlayerSettings.getInstance();

try (var ignored = monitor.begin("Re-encode audio file")) {
IOUtils.exec(settings.ffmpegPath, "-acodec", codec, "-i", input, "-ac", "2", output, "-y");
if (IOUtils.getExtension(output).equals(codec)) {
try (var ignored = monitor.begin("Copy audio file")) {
Files.copy(input, output);
}
} else {
try (var ignored = monitor.begin("Re-encode audio file")) {
IOUtils.exec(settings.ffmpegPath, "-acodec", codec, "-i", input, "-ac", "2", output, "-y");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.shade.util.NotNull;

public sealed interface Codec {
record Wwise() implements Codec {}
record Wem() implements Codec {}

record Wave(@NotNull String encoding) implements Codec {}
record Generic(@NotNull String name) implements Codec {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.shade.decima.ui.data.viewer.audio.data.echo;

import com.shade.platform.model.util.BufferUtils;
import com.shade.util.NotNull;

import java.nio.ByteBuffer;
import java.util.*;

public record EchoBank(@NotNull Map<Chunk.Type<?>, Chunk> chunks) {
private static final int ECHO_MAGIC = 'E' | 'C' << 8 | 'H' << 16 | 'O' << 24;
private static final int MEDA_MAGIC = 'M' | 'E' << 8 | 'D' << 16 | 'A' << 24;
private static final int STRL_MAGIC = 'S' | 'T' << 8 | 'R' << 16 | 'L' << 24;
private static final int PICD_MAGIC = 'P' | 'I' << 8 | 'C' << 16 | 'D' << 24;

public static EchoBank read(@NotNull ByteBuffer buffer) {
if (buffer.getInt() != ECHO_MAGIC || buffer.getInt() != -1) {
throw new IllegalArgumentException("Invalid bank format");
}

int size = buffer.getInt();
Map<Chunk.Type<?>, Chunk> chunks = new HashMap<>();

for (int i = 0; i < size / 12; i++) {
int magic = buffer.getInt();
int offset = buffer.getInt();
int length = buffer.getInt();

if (offset > buffer.limit()) {
throw new IllegalArgumentException("Invalid bank offset");
}

ByteBuffer data = buffer.slice(offset, length).order(buffer.order());

switch (magic) {
case MEDA_MAGIC -> chunks.put(Chunk.Type.MEDA, Chunk.Media.read(data));
case STRL_MAGIC -> chunks.put(Chunk.Type.STRL, Chunk.Names.read(data));
}
}

return new EchoBank(Collections.unmodifiableMap(chunks));
}

@NotNull
public <T extends Chunk> T get(@NotNull Chunk.Type<T> type) {
Chunk chunk = chunks.get(type);

if (chunk != null) {
return type.type().cast(chunk);
}

throw new NoSuchElementException("No such chunk: " + type.id());
}

public sealed interface Chunk {
record Type<T extends Chunk>(@NotNull String id, @NotNull Class<T> type) {
public static final Type<Media> MEDA = new Type<>("MEDA", Media.class);
public static final Type<Names> STRL = new Type<>("STRL", Names.class);
}

record Media(@NotNull Entry[] entries) implements Chunk {
public record Entry(int offset, int size) {
public static Entry read(@NotNull ByteBuffer buffer) {
int offset = Math.toIntExact(buffer.getLong());
int size = Math.toIntExact(buffer.getLong());
buffer.position(buffer.position() + 32);
return new Entry(offset, size);
}
}

public static Media read(@NotNull ByteBuffer buffer) {
if (buffer.getInt() != PICD_MAGIC) {
throw new IllegalArgumentException("Invalid media format");
}
int count = buffer.getInt();
if (buffer.getLong() != 0) {
throw new IllegalArgumentException("Expected padding");
}
return new Media(BufferUtils.getObjects(buffer, count, Entry[]::new, Entry::read));
}
}

record Names(@NotNull String[] names) implements Chunk {
@NotNull
public static Names read(@NotNull ByteBuffer buffer) {
List<String> names = new ArrayList<>();
while (buffer.hasRemaining()) {
names.add(BufferUtils.getString(buffer));
}
return new Names(names.toArray(String[]::new));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.shade.decima.ui.data.viewer.audio.data.mpeg;

/**
* Represents an MPEG audio frame header.
*
* @see <a href="https://www.datavoyage.com/mpgscript/mpeghdr.htm">MPEG Audio Layer I/II/III frame header</a>
*/
public record MpegFrameHeader(int raw) {
public static final int MPEG_1 = 0b1;
public static final int MPEG_2 = 0b0;

public static final int LAYER_1 = 0b11;
public static final int LAYER_2 = 0b10;
public static final int LAYER_3 = 0b01;

private static final int[] BITRATE_V1_L1 = {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
private static final int[] BITRATE_V1_L2 = {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
private static final int[] BITRATE_V1_L3 = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
private static final int[] BITRATE_V2_L1 = {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
private static final int[] BITRATE_V2_L2 = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};

public MpegFrameHeader {
if (raw >>> 20 != 0xfff) {
throw new IllegalArgumentException("Invalid frame sync");
}
}

public int samplesPerFrame() {
return switch (layerId()) {
case LAYER_1 -> 384;
case LAYER_2 -> 1152;
case LAYER_3 -> mpegId() == MPEG_1 ? 1152 : 576;
default -> throw new IllegalStateException();
};
}

public int samplingRate() {
int sampleRate = switch (frequencyIndex()) {
case 0 -> 44100;
case 1 -> 48000;
case 2 -> 32000;
default -> throw new IllegalStateException();
};
return mpegId() == MPEG_1 ? sampleRate : sampleRate >> 1;
}

public int bitRate() {
if (mpegId() == MPEG_1) {
if (layerId() == LAYER_1) {
return BITRATE_V1_L1[bitRateIndex()];
} else if (layerId() == LAYER_2) {
return BITRATE_V1_L2[bitRateIndex()];
} else {
return BITRATE_V1_L3[bitRateIndex()];
}
} else {
if (layerId() == LAYER_1) {
return BITRATE_V2_L1[bitRateIndex()];
} else {
return BITRATE_V2_L2[bitRateIndex()];
}
}
}

public int paddingSize() {
if (layerId() == LAYER_1) {
return 4;
} else {
return 1;
}
}

public int frameSize() {
int frameSize = bitRate() * 144000 / samplingRate();
if (channelMode() == 3) {
frameSize >>= 1;
}
if (protectionBit() == 0) {
frameSize -= 2;
}
if (paddingBit() == 1) {
frameSize += paddingSize();
}
return frameSize;
}

public int frameSync() {
return raw >>> 20;
}

public int mpegId() {
return raw >>> 19 & 1;
}

public int layerId() {
return raw >>> 17 & 3;
}

public int protectionBit() {
return raw >>> 16 & 1;
}

public int bitRateIndex() {
return raw >>> 12 & 15;
}

public int frequencyIndex() {
return raw >>> 10 & 3;
}

public int paddingBit() {
return raw >>> 9 & 1;
}

public int privateBit() {
return raw >>> 8 & 1;
}

public int channelMode() {
return raw >>> 6 & 3;
}

public int modeExtension() {
return raw >>> 4 & 3;
}

public int copyright() {
return raw >>> 3 & 1;
}

public int original() {
return raw >>> 2 & 1;
}

public int emphasis() {
return raw & 3;
}

@Override
public String toString() {
return String.format(
"MpegFrameHeader[frameSync=%x, mpegId=%d, layerId=%d, protectionBit=%d, bitrateIndex=%d, frequencyIndex=%d, paddingBit=%d, privateBit=%d, channelMode=%d, modeExtension=%d, copyright=%d, original=%d, emphasis=%d]",
frameSync(), mpegId(), layerId(), protectionBit(), bitRateIndex(), frequencyIndex(), paddingBit(), privateBit(), channelMode(), modeExtension(), copyright(), original(), emphasis()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.util.NotNull;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.util.NotNull;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.shade.decima.ui.data.viewer.audio.data.wwise;

public interface AkHircNode {
int id();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.util.NotNull;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.platform.model.util.BufferUtils;
import com.shade.util.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.util.NotNull;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.platform.model.util.BufferUtils;
import com.shade.util.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.shade.decima.ui.data.viewer.audio.wwise;
package com.shade.decima.ui.data.viewer.audio.data.wwise;

import com.shade.decima.ui.data.viewer.audio.AudioPlayerUtils;
import com.shade.util.NotNull;
Expand Down
Loading

0 comments on commit 9cabf8d

Please sign in to comment.