mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 09:48:44 +00:00
Merge pull request #5 from storytold/styling/tabs-css
Clip Offset added lipsync type.
This commit is contained in:
6 files changed
+202
-66
No files matched your search
@@ -1,6 +1,6 @@
|
||||
export interface ClipUI {
|
||||
version: number
|
||||
type: "transform" | "audio" | "animation"
|
||||
type: "transform" | "audio" | "animation" | "lipsync"
|
||||
media_id: string
|
||||
object_uuid: string
|
||||
start_offset: number // in frames so 60 frames per second on the timeline.
|
||||
@@ -11,7 +11,7 @@ export interface ClipUI {
|
||||
// it is created from a media id.
|
||||
export class ClipUI implements ClipUI {
|
||||
version: number
|
||||
type: "transform" | "audio" | "animation"
|
||||
type: "transform" | "audio" | "animation" | "lipsync"
|
||||
media_id: string
|
||||
object_uuid: string
|
||||
start_offset: number // in frames
|
||||
@@ -19,7 +19,7 @@ export class ClipUI implements ClipUI {
|
||||
|
||||
constructor(
|
||||
version: number,
|
||||
type: "transform" | "audio" | "animation",
|
||||
type: "transform" | "audio" | "animation" | "lipsync",
|
||||
media_id: string,
|
||||
object_uuid: string,
|
||||
start_offset: number,
|
||||
@@ -36,8 +36,8 @@ export class ClipUI implements ClipUI {
|
||||
toJSON(): string {
|
||||
return JSON.stringify({
|
||||
version: this.version,
|
||||
object_uuid: this.object_uuid,
|
||||
type: this.type,
|
||||
object_uuid: this.object_uuid,
|
||||
media_id: this.media_id,
|
||||
start_offset: this.start_offset,
|
||||
ending_offset: this.ending_offset
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
interface AudioData {
|
||||
audioContext: AudioContext;
|
||||
audioBuffer: AudioBuffer;
|
||||
}
|
||||
|
||||
class AudioData implements AudioData{
|
||||
audioContext: AudioContext;
|
||||
audioBuffer: AudioBuffer;
|
||||
source: AudioBufferSourceNode | undefined
|
||||
|
||||
constructor (audioContext: AudioContext, audioBuffer: AudioBuffer) {
|
||||
this.audioContext = audioContext;
|
||||
this.audioBuffer = audioBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LipSyncTrackClip {
|
||||
version: number;
|
||||
media_id: string;
|
||||
type: "lipsync";
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export class LipSyncTrackClip implements LipSyncTrackClip {
|
||||
version: number;
|
||||
media_id: string;
|
||||
type: "lipsync" = "lipsync";
|
||||
volume: number;
|
||||
audio_data: AudioData | undefined;
|
||||
|
||||
constructor(version: number, media_id: string, volume: number) {
|
||||
this.version = version;
|
||||
this.media_id = media_id;
|
||||
this.type = "lipsync";
|
||||
this.volume = volume;
|
||||
this.download_audio().then(data => {
|
||||
this.audio_data = data;
|
||||
});
|
||||
}
|
||||
|
||||
// lip sync will be generated through TTS
|
||||
async get_media_url() {
|
||||
//This is for prod when we have the proper info on the url.
|
||||
let api_base_url = "https://api.fakeyou.com";
|
||||
let url = `${api_base_url}/v1/media_files/file/${this.media_id}`
|
||||
let responce = await fetch(url);
|
||||
let json = await JSON.parse(await responce.text());
|
||||
let bucketPath = json["media_file"]["public_bucket_path"];
|
||||
let media_base_url = "https://storage.googleapis.com/vocodes-public"
|
||||
let media_url = `${media_base_url}${bucketPath}`
|
||||
return media_url;
|
||||
}
|
||||
|
||||
async download_audio() {
|
||||
let url = await this.get_media_url();
|
||||
const audioContext = new AudioContext();
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
return new AudioData(audioContext, audioBuffer);
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return JSON.stringify({
|
||||
version: this.version,
|
||||
media_id: this.media_id,
|
||||
type: this.type,
|
||||
volume: this.volume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faL } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { AudioTrackClip } from "../datastructures/clips/audio_track_clip";
|
||||
|
||||
|
||||
class AudioEngine {
|
||||
clips: { [key: string]: AudioTrackClip } = {};
|
||||
audio_sources: { [key: string]: AudioBufferSourceNode } = {};
|
||||
@@ -15,11 +14,13 @@ class AudioEngine {
|
||||
this.version = 1.0;
|
||||
}
|
||||
|
||||
// loads clips into the engine to cache
|
||||
loadClip(audio_media_id: string) {
|
||||
if(this.clips[audio_media_id] != null) { return; }
|
||||
this.clips[audio_media_id] = new AudioTrackClip(this.version, audio_media_id, 1.0);
|
||||
}
|
||||
|
||||
// plays from the timeline.
|
||||
playClip(audio_media_id: string) {
|
||||
if (this.clips[audio_media_id] == null) { this.loadClip(audio_media_id); }
|
||||
let clip = this.clips[audio_media_id];
|
||||
@@ -39,6 +40,7 @@ class AudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// stops the clips
|
||||
stopClip(audio_media_id: string) {
|
||||
const clip = this.clips[audio_media_id];
|
||||
if (clip.audio_data?.source) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
import { LipSyncTrackClip } from "../datastructures/clips/lipsync_track_clip";
|
||||
import { LipSync } from "./lipsync";
|
||||
import * as THREE from 'three';
|
||||
|
||||
// needs to be apart of the editor window.
|
||||
export class LipSyncEngine {
|
||||
|
||||
clips: { [key: string]: LipSyncTrackClip } = {}
|
||||
audio_sources: { [key: string]: AudioBufferSourceNode } = {}
|
||||
version: number
|
||||
lipsync: LipSync
|
||||
|
||||
constructor() {
|
||||
|
||||
this.clips = {}
|
||||
this.audio_sources = {}
|
||||
this.version = 1.0
|
||||
// we might need 3 of these one for each character ...
|
||||
this.lipsync = new LipSync()
|
||||
}
|
||||
|
||||
loadClip(audio_media_id: string) {
|
||||
this.clips[audio_media_id] = new LipSyncTrackClip(this.version, audio_media_id, 1.0);
|
||||
}
|
||||
|
||||
// needs to be called by the engine.
|
||||
update(delta_time: number) {
|
||||
this.lipsync.update(delta_time)
|
||||
}
|
||||
|
||||
playClip(face: THREE.Object3D,audio_media_id: string) {
|
||||
// face movement here
|
||||
this.lipsync.face = face
|
||||
const clip = this.clips[audio_media_id];
|
||||
// this is a buffer right?
|
||||
this.lipsync.startLipSyncFromAudioBuffer(clip)
|
||||
}
|
||||
|
||||
stopClip(audio_media_id: string) {
|
||||
const clip = this.clips[audio_media_id];
|
||||
if (clip.audio_data?.source) {
|
||||
clip.audio_data.source.stop();
|
||||
} else {
|
||||
console.warn(`AudioManager: AudioClip with id "${audio_media_id}" not found.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LipSyncEngine;
|
||||
@@ -7,32 +7,26 @@ const samplingFrequency = 44100;
|
||||
|
||||
for (let m = 0; m < BoundingFrequencyMasc.length; m++) {
|
||||
IndicesFrequencyMale[m] = Math.round(
|
||||
((2 * FFT_SIZE) / samplingFrequency) * BoundingFrequencyMasc[m]
|
||||
((2 * FFT_SIZE) / samplingFrequency) * BoundingFrequencyMasc[m],
|
||||
);
|
||||
}
|
||||
|
||||
for (let m = 0; m < BoundingFrequencyFem.length; m++) {
|
||||
IndicesFrequencyFemale[m] = Math.round(
|
||||
((2 * FFT_SIZE) / samplingFrequency) * BoundingFrequencyFem[m]
|
||||
((2 * FFT_SIZE) / samplingFrequency) * BoundingFrequencyFem[m],
|
||||
);
|
||||
}
|
||||
|
||||
export class LipSync {
|
||||
constructor(vrm) {
|
||||
this.vrm = vrm;
|
||||
|
||||
this.ee = this.vrm.morphTargetDictionary["E"];
|
||||
this.oh = this.vrm.morphTargetDictionary["O"];
|
||||
this.ah = this.vrm.morphTargetDictionary["aa"];
|
||||
|
||||
const update = (deltaTime, elapsedTime) => {
|
||||
requestAnimationFrame(update);
|
||||
this.update(deltaTime, elapsedTime);
|
||||
};
|
||||
|
||||
update();
|
||||
constructor(face) {
|
||||
this.face = face;
|
||||
// we have to modify this for our
|
||||
this.ee = this.face.morphTargetDictionary["E"];
|
||||
this.oh = this.face.morphTargetDictionary["O"];
|
||||
this.ah = this.face.morphTargetDictionary["aa"];
|
||||
}
|
||||
|
||||
// for streaming sound?
|
||||
start(stream) {
|
||||
this.audioContext = new AudioContext();
|
||||
this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
|
||||
@@ -40,6 +34,7 @@ export class LipSync {
|
||||
this.mediaStreamSource.connect(this.meter);
|
||||
}
|
||||
|
||||
// testing only
|
||||
startFromAudioFile(file) {
|
||||
if (!this.audioContext) this.audioContext = new AudioContext();
|
||||
|
||||
@@ -63,6 +58,25 @@ export class LipSync {
|
||||
});
|
||||
}
|
||||
|
||||
startLipSyncFromAudioBuffer(buffer) {
|
||||
if (!this.audioContext) this.audioContext = new AudioContext();
|
||||
if (!this.userSpeechAnalyzer)
|
||||
this.userSpeechAnalyzer = this.audioContext.createAnalyser();
|
||||
this.userSpeechAnalyzer.smoothingTimeConstant = 0.5;
|
||||
this.userSpeechAnalyzer.fftSize = FFT_SIZE;
|
||||
|
||||
if (this.mediaStreamSource) this.mediaStreamSource.stop();
|
||||
|
||||
this.mediaStreamSource = this.audioContext.createBufferSource();
|
||||
this.mediaStreamSource.buffer = buffer;
|
||||
this.meter = LipSync.createAudioMeter(this.audioContext);
|
||||
this.mediaStreamSource.connect(this.meter);
|
||||
this.mediaStreamSource.connect(this.audioContext.destination);
|
||||
this.mediaStreamSource.start();
|
||||
// connect the output of mediaStreamSource to the input of userSpeechAnalyzer
|
||||
this.mediaStreamSource.connect(this.userSpeechAnalyzer);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.meter?.shutdown();
|
||||
this.meter = null;
|
||||
@@ -70,23 +84,20 @@ export class LipSync {
|
||||
return this.audioContext?.close().catch(() => {}) || Promise.resolve();
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
update() {
|
||||
if (this.meter) {
|
||||
const { volume } = this.meter;
|
||||
if (volume < 0.01) {
|
||||
// get the shape keys from the VRM avatar
|
||||
// get the shape keys from the face avatar
|
||||
|
||||
this.vrm.morphTargetInfluences[this.ee] = 0;
|
||||
this.vrm.morphTargetInfluences[this.ah] = 0;
|
||||
this.vrm.morphTargetInfluences[this.oh] = 0;
|
||||
this.face.morphTargetInfluences[this.ee] = 0;
|
||||
this.face.morphTargetInfluences[this.ah] = 0;
|
||||
this.face.morphTargetInfluences[this.oh] = 0;
|
||||
} else {
|
||||
const { ah, oh, ee } = this.update2();
|
||||
this.vrm.morphTargetInfluences[this.ee] = ee;
|
||||
this.vrm.morphTargetInfluences[this.ah] = ah;
|
||||
this.vrm.morphTargetInfluences[this.oh] = oh;
|
||||
|
||||
// update the shape keys
|
||||
//this.vrm.expressionManager.update(deltaTime);
|
||||
this.face.morphTargetInfluences[this.ee] = ee;
|
||||
this.face.morphTargetInfluences[this.ah] = ah;
|
||||
this.face.morphTargetInfluences[this.oh] = oh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +113,7 @@ export class LipSync {
|
||||
}
|
||||
|
||||
const spectrum = new Float32Array(
|
||||
this.userSpeechAnalyzer.frequencyBinCount
|
||||
this.userSpeechAnalyzer.frequencyBinCount,
|
||||
);
|
||||
// Populate frequency data for computing frequency intensities
|
||||
this.userSpeechAnalyzer.getFloatFrequencyData(spectrum); // getByteTimeDomainData gets volumes over the sample time
|
||||
@@ -203,4 +214,4 @@ export class LipSync {
|
||||
|
||||
return processor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AnyJson } from "three/examples/jsm/nodes/core/constants.js";
|
||||
import { ClipUI } from "../datastructures/clips/clip_offset";
|
||||
|
||||
import Scene from "./scene.js";
|
||||
import AudioEngine from "./audio_engine";
|
||||
import TransformEngine from "./transform_engine";
|
||||
import Scene from "./scene.js";
|
||||
import * as THREE from 'three';
|
||||
import { LipSyncEngine } from "./lip_sync_engine";
|
||||
import { AnimationEngine } from "./animation_engine";
|
||||
|
||||
// Every object uuid / entity has a track.
|
||||
export class TimelineCurrentReactState {
|
||||
@@ -27,14 +29,22 @@ export class TimeLine {
|
||||
|
||||
timelineState: TimelineCurrentReactState
|
||||
|
||||
// plays audio
|
||||
audioEngine: AudioEngine
|
||||
// key framing
|
||||
transformEngine: TransformEngine
|
||||
// animation engine
|
||||
animationEngine: AnimationEngine
|
||||
// lip sync engine
|
||||
lipSyncEngine: LipSyncEngine
|
||||
|
||||
scene: Scene
|
||||
|
||||
constructor(audioEngine:AudioEngine, transformEngine:TransformEngine, scene:Scene) {
|
||||
// ensure that the elements are loaded first.
|
||||
constructor(audioEngine:AudioEngine,
|
||||
transformEngine:TransformEngine,
|
||||
lipsyncEngine:LipSyncEngine,
|
||||
animationEngine:AnimationEngine,
|
||||
scene:Scene) {
|
||||
this.timelineItems = []
|
||||
this.timeLineLimit = 60 * 10 // 10 seconds
|
||||
this.runningClips = []
|
||||
@@ -45,6 +55,9 @@ export class TimeLine {
|
||||
// this will be used to play the audio clips
|
||||
this.audioEngine = audioEngine
|
||||
this.transformEngine = transformEngine
|
||||
this.lipSyncEngine = lipsyncEngine
|
||||
this.animationEngine = animationEngine
|
||||
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
@@ -98,27 +111,6 @@ export class TimeLine {
|
||||
async play(): Promise<void> {
|
||||
console.log(`Starting Timeline`)
|
||||
this.isPlaying = true
|
||||
// const updateInterval = 100
|
||||
// const timerID = setInterval(async () => {
|
||||
// console.log(`Current Time:${start}`)
|
||||
|
||||
// // get last updated time when stopped
|
||||
// for (const element of this.timelineItems) {
|
||||
// if (element.start_offset >= start) {
|
||||
// // run async
|
||||
// // element.play()
|
||||
// // remove the element
|
||||
// this.timelineItems = this.timelineItems.filter(item => item !== element)
|
||||
// }
|
||||
// }
|
||||
// if (start == this.timeLineLimit || this.timelineItems.length == 0) {
|
||||
// this.stop()
|
||||
// }
|
||||
|
||||
// start += updateInterval
|
||||
// }, updateInterval)
|
||||
|
||||
// this.timerID = timerID
|
||||
}
|
||||
|
||||
async reset_scene() {
|
||||
@@ -131,7 +123,8 @@ export class TimeLine {
|
||||
this.audioEngine.loadClip(element.media_id);
|
||||
}
|
||||
else if (element.type == "animation") {
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.stop()
|
||||
throw "Error New Type of element in the timeline"
|
||||
}
|
||||
@@ -155,20 +148,28 @@ export class TimeLine {
|
||||
// run async
|
||||
// element.play()
|
||||
// remove the element from the list
|
||||
let object = this.scene.get_object_by_uuid(element.object_uuid)
|
||||
if (element.type == "transform") {
|
||||
let object = this.scene.get_object_by_uuid(element.object_uuid);
|
||||
if(object)
|
||||
{
|
||||
this.transformEngine.clips[element.object_uuid].length = (element.ending_offset-element.start_offset);
|
||||
this.transformEngine.clips[element.object_uuid].step(object);
|
||||
this.transformEngine.clips[element.object_uuid].length = (element.ending_offset-element.start_offset)
|
||||
this.transformEngine.clips[element.object_uuid].step(object)
|
||||
}
|
||||
}
|
||||
else if (element.type == "audio") {
|
||||
this.audioEngine.playClip(element.media_id);
|
||||
else if (element.type == "audio") {
|
||||
// global audio track
|
||||
this.audioEngine.playClip(element.media_id)
|
||||
}
|
||||
else if (element.type == "lipsync") {
|
||||
// I think you just get the object verify it is a character ? then play the clip, but ... it needs
|
||||
// need character face
|
||||
this.lipSyncEngine.playClip(object,element.media_id)
|
||||
}
|
||||
|
||||
else if (element.type == "animation") {
|
||||
|
||||
} else {
|
||||
// use the media id to figure out which animation clip belongs to who
|
||||
}
|
||||
else {
|
||||
this.stop()
|
||||
throw "Error New Type of element in the timeline"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user