Skip to content

Commit

Permalink
Add wave exporter.
Browse files Browse the repository at this point in the history
Fix the problem where the internal volume of decoded wave are reduced by 0.5x.
  • Loading branch information
okaxaki committed Aug 19, 2023
1 parent c12c22f commit c992a42
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 94 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "88play",
"private": true,
"version": "1.2.3",
"version": "1.3.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
4 changes: 4 additions & 0 deletions src/contexts/EditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface EditorContextState {
resourceMap: MMLResourceMap;
unresolvedResources: MMLResourceEntry[];
busy: boolean;
progress: number | null;
progressMessage: string | null;
openFile: () => void;
}

Expand All @@ -31,6 +33,8 @@ const defaultContextState: EditorContextState = {
resourceMap: {},
unresolvedResources: [],
busy: false,
progress: null,
progressMessage: null,
openFile: () => {
// noop
},
Expand Down
8 changes: 8 additions & 0 deletions src/contexts/EditorContextReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export class EditorContextReducer {
this.setState((state) => ({ ...state, busy: value }));
}

setProgress(value: number | null) {
this.setState((state) => ({ ...state, progress: value }));
}

setProgressMessage(value: string | null) {
this.setState((state) => ({ ...state, progressMessage: value }));
}

loadAsMML = async (
file: File | URL | string
): Promise<[string | null, MMLResourceMap | null]> => {
Expand Down
10 changes: 7 additions & 3 deletions src/contexts/PlayerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const createDefaultContextState = () => {
playStateChangeCount: 0,
playState: "stopped",
busy: false,
masterGain: 4.0,
masterGain: 2.0,
unmute: async () => {
unmuteAudio();
if (audioContext.state != "running") {
Expand All @@ -73,7 +73,11 @@ const createDefaultContextState = () => {
try {
const data = localStorage.getItem("88play.playerContext");
const json = data != null ? JSON.parse(data) : {};
state.masterGain = json.masterGain ?? state.masterGain;
if (json.version == 1) {
state.masterGain = json.masterGain / 2 ?? state.masterGain;
} else {
state.masterGain = json.masterGain ?? state.masterGain;
}
state.gainNode.gain.value = state.masterGain;
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -165,7 +169,7 @@ function useExternalSyncEffect(state: PlayerContextState) {

useEffect(() => {
const { masterGain } = state;
const data = { version: 1, masterGain };
const data = { version: 2, masterGain };
localStorage.setItem("88play.playerContext", JSON.stringify(data));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.masterGain]);
Expand Down
88 changes: 88 additions & 0 deletions src/mucom/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Mucom88, { MucomStatusType } from "mucom88-js";

export async function loadAsset(url: string): Promise<Uint8Array> {
const res = await fetch(new URL(url, import.meta.url));
return new Uint8Array(await res.arrayBuffer());
}

const maxDuration = 60 * 1000 * 10;
const defaultFadeDuration = 5 * 1000;

export class Fader {
durationInFrame = 0;
fadeDurationInFrame = 0;
fadeStartFrame?: number | null;
timeTagValue?: number | null;
maxCount = 0;

setup(
mucom: Mucom88,
sampleRate: number,
timeTagValue?: number | null,
fadeTagValue?: number | null
) {
this.timeTagValue = timeTagValue;
if (this.timeTagValue != null) {
const duration = Math.min(60 * 20 * 1000, this.timeTagValue * 1000);
this.durationInFrame = Math.floor((sampleRate * duration) / 1000);
} else {
this.durationInFrame = Math.floor((sampleRate * maxDuration) / 1000);
}
this.fadeDurationInFrame = Math.floor(
sampleRate * (fadeTagValue ?? defaultFadeDuration / 1000)
);
this.fadeStartFrame = null;

const { maxCount, hasGlobalLoop } = mucom.getCountData();
this.maxCount = maxCount;

if (!hasGlobalLoop) {
this.fadeDurationInFrame = 0;
}
}

updateFadeState(currentFrame: number, mucom: Mucom88) {
if (this.fadeStartFrame == null) {
if (currentFrame >= this.durationInFrame - this.fadeDurationInFrame) {
this.fadeStartFrame = currentFrame;
}
if (this.timeTagValue == null) {
const curCount = mucom.getStatus(MucomStatusType.INTCOUNT);
if (this.maxCount <= curCount) {
this.fadeStartFrame = currentFrame;
}
}
}
}

getValue(currentFrame: number): number {
if (this.fadeStartFrame != null) {
const elapsed = currentFrame - this.fadeStartFrame;
return Math.min(
1.0,
Math.max(
0,
(this.fadeDurationInFrame - elapsed) / this.fadeDurationInFrame
)
);
}
return 1.0;
}
}

export function getTimeFadeTagValue(mml: string): {
time: number | null;
fade: number | null;
} {
const matches = mml.matchAll(/^#(time|fade)\s+([0-9]+).*$/gm);
let time = null;
let fade = null;
for (const match of matches) {
if (match[1] == "time") {
time = parseInt(match[2]);
} else {
fade = parseInt(match[2]);
}
}
return { time, fade };
}
92 changes: 5 additions & 87 deletions src/mucom/mucom-decoder-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Mucom88, { MucomStatusType, CHDATA } from "mucom88-js";
import Mucom88, { CHDATA } from "mucom88-js";
import { AudioDecoderWorker } from "webaudio-stream-player";

import bd from "../assets/wav/2608_bd.wav";
Expand All @@ -7,6 +7,7 @@ import rim from "../assets/wav/2608_rim.wav";
import sd from "../assets/wav/2608_sd.wav";
import tom from "../assets/wav/2608_tom.wav";
import top from "../assets/wav/2608_top.wav";
import { Fader, getTimeFadeTagValue, loadAsset } from "./common";

export type MucomDecoderAttachment = {
type: "pcm" | "voice";
Expand All @@ -26,76 +27,6 @@ export type MucomDecoderSnapshot = {
data: (CHDATA | null)[];
};

const maxDuration = 60 * 1000 * 10;
const defaultFadeDuration = 5 * 1000;

async function loadAsset(url: string): Promise<Uint8Array> {
const res = await fetch(new URL(url, import.meta.url));
return new Uint8Array(await res.arrayBuffer());
}

class Fader {
durationInFrame = 0;
fadeDurationInFrame = 0;
fadeStartFrame?: number | null;
timeTagValue?: number | null;
maxCount = 0;

setup(
mucom: Mucom88,
sampleRate: number,
timeTagValue?: number | null,
fadeTagValue?: number | null
) {
this.timeTagValue = timeTagValue;
if (this.timeTagValue != null) {
const duration = Math.min(60 * 20 * 1000, this.timeTagValue * 1000);
this.durationInFrame = Math.floor((sampleRate * duration) / 1000);
} else {
this.durationInFrame = Math.floor((sampleRate * maxDuration) / 1000);
}
this.fadeDurationInFrame = Math.floor(
sampleRate * (fadeTagValue ?? defaultFadeDuration / 1000)
);
this.fadeStartFrame = null;

const { maxCount, hasGlobalLoop } = mucom.getCountData();
this.maxCount = maxCount;

if (!hasGlobalLoop) {
this.fadeDurationInFrame = 0;
}
}

updateFadeState(currentFrame: number, mucom: Mucom88) {
if (this.fadeStartFrame == null) {
if (currentFrame >= this.durationInFrame - this.fadeDurationInFrame) {
this.fadeStartFrame = currentFrame;
}
if (this.timeTagValue == null) {
const curCount = mucom.getStatus(MucomStatusType.INTCOUNT);
if (this.maxCount <= curCount) {
this.fadeStartFrame = currentFrame;
}
}
}
}

getValue(currentFrame: number): number {
if (this.fadeStartFrame != null) {
const elapsed = currentFrame - this.fadeStartFrame;
return Math.min(
1.0,
Math.max(
0,
(this.fadeDurationInFrame - elapsed) / this.fadeDurationInFrame
)
);
}
return 1.0;
}
}

class MucomDecoderWorker extends AudioDecoderWorker {
constructor(worker: Worker) {
super(worker);
Expand All @@ -118,19 +49,6 @@ class MucomDecoderWorker extends AudioDecoderWorker {
Mucom88.FS.writeFile("/2608_TOP.WAV", await loadAsset(top));
}

getTagValue(mml: string): { time: number | null; fade: number | null } {
const matches = mml.matchAll(/^#(time|fade)\s+([0-9]+).*$/gm);
let time = null;
let fade = null;
for (const match of matches) {
if (match[1] == "time") {
time = parseInt(match[2]);
} else {
fade = parseInt(match[2]);
}
}
return { time, fade };
}

async start(args: MucomDecoderStartOptions): Promise<void> {
if (this._mucom == null) {
Expand All @@ -145,7 +63,7 @@ class MucomDecoderWorker extends AudioDecoderWorker {
this._fader = new Fader();
this._decodeFrames = 0;

const { time, fade } = this.getTagValue(args.mml);
const { time, fade } = getTimeFadeTagValue(args.mml);
this._mucom.reset(this.sampleRate);
this._mucom.loadMML(args.mml);
this._fader.setup(this._mucom, this.sampleRate, time, fade);
Expand Down Expand Up @@ -192,8 +110,8 @@ class MucomDecoderWorker extends AudioDecoderWorker {
snapshots.push(this.getChannelSnapshot());
for (let j = 0; j < step; j++) {
const fade = this._fader?.getValue(this._decodeFrames + j) ?? 1.0;
lch[i + j] = Math.round((fade * buf[j * 2]) >> 1);
rch[i + j] = Math.round((fade * buf[j * 2 + 1]) >> 1);
lch[i + j] = Math.round(fade * buf[j * 2]);
rch[i + j] = Math.round(fade * buf[j * 2 + 1]);
}
this._decodeFrames += step;
i += step;
Expand Down
Loading

0 comments on commit c992a42

Please sign in to comment.