ReaWwise

REAPER extension
Log | Files | Refs | Submodules

commit 0d55c704825ebafeedb31d15382e03c8b89f0235
parent c0c499065671ba178ffcd0c3fab9af74941fdd44
Author: Pascal Maheux <pmaheux@audiokinetic.com>
Date:   Mon, 12 Sep 2022 15:06:24 -0400

Various bug fixes

* Ensures that all calls to reaper are done on the main thread

Change-Id: I05761533548a910a7b4860da07c597370060f09d

Diffstat:
M3rd/reaper-sdk/sdk/reaper_plugin_functions.h | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
MPackage.distribute | 11+++++++----
MPackage.instrument | 4----
MReadme.md | 19+++++++++++++------
Msrc/extension/CMakeLists.txt | 13+++++--------
Msrc/extension/Extension.cpp | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/extension/ExtensionWindow.cpp | 1-
Msrc/extension/ReaperContext.cpp | 281+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/extension/ReaperContext.h | 80++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/shared/Core/AssertHook.cpp | 2+-
Msrc/shared/Core/DawContext.h | 1+
Msrc/shared/Core/DawWatcher.cpp | 271+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/shared/Core/DawWatcher.h | 30++++++++++++++----------------
Msrc/shared/Core/ImportTask.h | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/shared/Core/Logger.cpp | 2+-
Msrc/shared/Core/WaapiClient.cpp | 4++--
Msrc/shared/Core/WaapiClient.h | 26++++++++++++++++++++++----
Msrc/shared/Model/Import.h | 50+++++++++++++++++++++++++++++++++++---------------
Msrc/shared/Persistance/ApplicationProperties.cpp | 17+++++++++++++----
Msrc/shared/Persistance/ApplicationProperties.h | 1+
Msrc/shared/Persistance/ApplicationStateValidator.cpp | 2+-
Msrc/shared/UI/ImportControlsComponent.cpp | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/shared/UI/ImportControlsComponent.h | 3+++
Msrc/shared/UI/PresetMenuComponent.cpp | 73+++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/shared/UI/PresetMenuComponent.h | 2--
Msrc/shared/UI/SelectedRowPropertiesComponent.cpp | 4+---
Msrc/standalone/StubContext.h | 5+++++
27 files changed, 887 insertions(+), 504 deletions(-)

diff --git a/3rd/reaper-sdk/sdk/reaper_plugin_functions.h b/3rd/reaper-sdk/sdk/reaper_plugin_functions.h @@ -2,7 +2,7 @@ #define _REAPER_PLUGIN_FUNCTIONS_H_ // REAPER API functions -// Generated by REAPER v6.45/win64 +// Generated by REAPER v6.67+dev0914/win32 /* * Copyright 2006 and later, Cockos Incorporated @@ -276,6 +276,14 @@ REAPERAPI_DEF //============================================== void (*BypassFxAllTracks)(int bypass); #endif +#if defined(REAPERAPI_WANT_CalcMediaSrcLoudness) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// CalcMediaSrcLoudness +// Calculates loudness statistics of media via dry run render. Statistics will be displayed to the user; call GetSetProjectInfo_String("RENDER_STATS") to retrieve via API. Returns 1 if loudness was calculated successfully, -1 if user canceled the dry run render. + + int (*CalcMediaSrcLoudness)(PCM_source* mediasource); +#endif + #if defined(REAPERAPI_WANT_CalculateNormalization) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // CalculateNormalization @@ -2056,6 +2064,7 @@ REAPERAPI_DEF //============================================== // I_RECARM : int * : record armed, 0=not record armed, 1=record armed // I_RECINPUT : int * : record input, <0=no input. if 4096 set, input is MIDI and low 5 bits represent channel (0=all, 1-16=only chan), next 6 bits represent physical input (63=all, 62=VKB). If 4096 is not set, low 10 bits (0..1023) are input start channel (ReaRoute/Loopback start at 512). If 2048 is set, input is multichannel input (using track channel count), or if 1024 is set, input is stereo input, otherwise input is mono. // I_RECMODE : int * : record mode, 0=input, 1=stereo out, 2=none, 3=stereo out w/latency compensation, 4=midi output, 5=mono out, 6=mono out w/ latency compensation, 7=midi overdub, 8=midi replace +// I_RECMODE_FLAGS : int * : record mode flags, &3=output recording mode (0=post fader, 1=pre-fx, 2=post-fx/pre-fader) // I_RECMON : int * : record monitoring, 0=off, 1=normal, 2=not when playing (tape style) // I_RECMONITEMS : int * : monitor items while recording, 0=off, 1=on // B_AUTO_RECARM : bool * : automatically set record arm when selected (does not immediately affect recarm state, script should set directly if desired) @@ -2083,14 +2092,15 @@ REAPERAPI_DEF //============================================== // D_DUALPANL : double * : dualpan position 1, -1..1, only if I_PANMODE==6 // D_DUALPANR : double * : dualpan position 2, -1..1, only if I_PANMODE==6 // I_PANMODE : int * : pan mode, 0=classic 3.x, 3=new balance, 5=stereo pan, 6=dual pan -// D_PANLAW : double * : pan law of track, <0=project default, 1=+0dB, etc +// D_PANLAW : double * : pan law of track, <0=project default, 0.5=-6dB, 0.707..=-3dB, 1=+0dB, 1.414..=-3dB with gain compensation, 2=-6dB with gain compensation, etc +// I_PANLAW_FLAGS : int * : pan law flags, 0=sine taper, 1=hybrid taper with deprecated behavior when gain compensation enabled, 2=linear taper, 3=hybrid taper // P_ENV:<envchunkname or P_ENV:{GUID... : TrackEnvelope * : (read-only) chunkname can be <VOLENV, <PANENV, etc; GUID is the stringified envelope GUID. // B_SHOWINMIXER : bool * : track control panel visible in mixer (do not use on master track) // B_SHOWINTCP : bool * : track control panel visible in arrange view (do not use on master track) // B_MAINSEND : bool * : track sends audio to parent // C_MAINSEND_OFFS : char * : channel offset of track send to parent // C_MAINSEND_NCH : char * : channel count of track send to parent (0=use all child track channels, 1=use one channel only) -// B_FREEMODE : bool * : track free item positioning enabled (call UpdateTimeline() after changing) +// I_FREEMODE : int * : 1=track free item positioning enabled, 2=track fixed lanes enabled (call UpdateTimeline() after changing) // C_BEATATTACHMODE : char * : track timebase, -1=project default, 0=time, 1=beats (position, length, rate), 2=beats (position only) // F_MCP_FXSEND_SCALE : float * : scale of fx+send area in MCP (0=minimum allowed, 1=maximum allowed) // F_MCP_FXPARM_SCALE : float * : scale of fx parameter area in MCP (0=minimum allowed, 1=maximum allowed) @@ -2647,7 +2657,9 @@ REAPERAPI_DEF //============================================== // P_ICON : const char * : track icon (full filename, or relative to resource_path/data/track_icons) // P_MCP_LAYOUT : const char * : layout name // P_RAZOREDITS : const char * : list of razor edit areas, as space-separated triples of start time, end time, and envelope GUID string. -// Example: "0.00 1.00 \"\" 0.00 1.00 "{xyz-...}" +// Example: "0.0 1.0 \"\" 0.0 1.0 "{xyz-...}" +// P_RAZOREDITS_EXT : const char * : list of razor edit areas, as comma-separated sets of space-separated tuples of start time, end time, optional: envelope GUID string, fixed/fipm top y-position, fixed/fipm bottom y-position. +// Example: "0.0 1.0,0.0 1.0 "{xyz-...}",1.0 2.0 "" 0.25 0.75" // P_TCP_LAYOUT : const char * : layout name // P_EXT:xyz : char * : extension-specific persistent data // P_UI_RECT:tcp.mute : char * : read-only, allows querying screen position + size of track WALTER elements (tcp.size queries screen position and size of entire TCP, etc). @@ -2662,6 +2674,7 @@ REAPERAPI_DEF //============================================== // I_RECARM : int * : record armed, 0=not record armed, 1=record armed // I_RECINPUT : int * : record input, <0=no input. if 4096 set, input is MIDI and low 5 bits represent channel (0=all, 1-16=only chan), next 6 bits represent physical input (63=all, 62=VKB). If 4096 is not set, low 10 bits (0..1023) are input start channel (ReaRoute/Loopback start at 512). If 2048 is set, input is multichannel input (using track channel count), or if 1024 is set, input is stereo input, otherwise input is mono. // I_RECMODE : int * : record mode, 0=input, 1=stereo out, 2=none, 3=stereo out w/latency compensation, 4=midi output, 5=mono out, 6=mono out w/ latency compensation, 7=midi overdub, 8=midi replace +// I_RECMODE_FLAGS : int * : record mode flags, &3=output recording mode (0=post fader, 1=pre-fx, 2=post-fx/pre-fader) // I_RECMON : int * : record monitoring, 0=off, 1=normal, 2=not when playing (tape style) // I_RECMONITEMS : int * : monitor items while recording, 0=off, 1=on // B_AUTO_RECARM : bool * : automatically set record arm when selected (does not immediately affect recarm state, script should set directly if desired) @@ -2689,14 +2702,15 @@ REAPERAPI_DEF //============================================== // D_DUALPANL : double * : dualpan position 1, -1..1, only if I_PANMODE==6 // D_DUALPANR : double * : dualpan position 2, -1..1, only if I_PANMODE==6 // I_PANMODE : int * : pan mode, 0=classic 3.x, 3=new balance, 5=stereo pan, 6=dual pan -// D_PANLAW : double * : pan law of track, <0=project default, 1=+0dB, etc +// D_PANLAW : double * : pan law of track, <0=project default, 0.5=-6dB, 0.707..=-3dB, 1=+0dB, 1.414..=-3dB with gain compensation, 2=-6dB with gain compensation, etc +// I_PANLAW_FLAGS : int * : pan law flags, 0=sine taper, 1=hybrid taper with deprecated behavior when gain compensation enabled, 2=linear taper, 3=hybrid taper // P_ENV:<envchunkname or P_ENV:{GUID... : TrackEnvelope * : (read-only) chunkname can be <VOLENV, <PANENV, etc; GUID is the stringified envelope GUID. // B_SHOWINMIXER : bool * : track control panel visible in mixer (do not use on master track) // B_SHOWINTCP : bool * : track control panel visible in arrange view (do not use on master track) // B_MAINSEND : bool * : track sends audio to parent // C_MAINSEND_OFFS : char * : channel offset of track send to parent // C_MAINSEND_NCH : char * : channel count of track send to parent (0=use all child track channels, 1=use one channel only) -// B_FREEMODE : bool * : track free item positioning enabled (call UpdateTimeline() after changing) +// I_FREEMODE : int * : 1=track free item positioning enabled, 2=track fixed lanes enabled (call UpdateTimeline() after changing) // C_BEATATTACHMODE : char * : track timebase, -1=project default, 0=time, 1=beats (position, length, rate), 2=beats (position only) // F_MCP_FXSEND_SCALE : float * : scale of fx+send area in MCP (0=minimum allowed, 1=maximum allowed) // F_MCP_FXPARM_SCALE : float * : scale of fx parameter area in MCP (0=minimum allowed, 1=maximum allowed) @@ -2717,7 +2731,9 @@ REAPERAPI_DEF //============================================== // P_ICON : const char * : track icon (full filename, or relative to resource_path/data/track_icons) // P_MCP_LAYOUT : const char * : layout name // P_RAZOREDITS : const char * : list of razor edit areas, as space-separated triples of start time, end time, and envelope GUID string. -// Example: "0.00 1.00 \"\" 0.00 1.00 "{xyz-...}" +// Example: "0.0 1.0 \"\" 0.0 1.0 "{xyz-...}" +// P_RAZOREDITS_EXT : const char * : list of razor edit areas, as comma-separated sets of space-separated tuples of start time, end time, optional: envelope GUID string, fixed/fipm top y-position, fixed/fipm bottom y-position. +// Example: "0.0 1.0,0.0 1.0 "{xyz-...}",1.0 2.0 "" 0.25 0.75" // P_TCP_LAYOUT : const char * : layout name // P_EXT:xyz : char * : extension-specific persistent data // P_UI_RECT:tcp.mute : char * : read-only, allows querying screen position + size of track WALTER elements (tcp.size queries screen position and size of entire TCP, etc). @@ -2769,18 +2785,22 @@ REAPERAPI_DEF //============================================== // GetSetProjectInfo // Get or set project information. // RENDER_SETTINGS : &(1|2)=0:master mix, &1=stems+master mix, &2=stems only, &4=multichannel tracks to multichannel files, &8=use render matrix, &16=tracks with only mono media to mono files, &32=selected media items, &64=selected media items via master, &128=selected tracks via master, &256=embed transients if format supports, &512=embed metadata if format supports, &1024=embed take markers if format supports, &2048=2nd pass render -// RENDER_BOUNDSFLAG : 0=custom time bounds, 1=entire project, 2=time selection, 3=all project regions, 4=selected media items, 5=selected project regions +// RENDER_BOUNDSFLAG : 0=custom time bounds, 1=entire project, 2=time selection, 3=all project regions, 4=selected media items, 5=selected project regions, 6=all project markers, 7=selected project markers // RENDER_CHANNELS : number of channels in rendered file // RENDER_SRATE : sample rate of rendered file (or 0 for project sample rate) // RENDER_STARTPOS : render start time when RENDER_BOUNDSFLAG=0 // RENDER_ENDPOS : render end time when RENDER_BOUNDSFLAG=0 -// RENDER_TAILFLAG : apply render tail setting when rendering: &1=custom time bounds, &2=entire project, &4=time selection, &8=all project regions, &16=selected media items, &32=selected project regions +// RENDER_TAILFLAG : apply render tail setting when rendering: &1=custom time bounds, &2=entire project, &4=time selection, &8=all project markers/regions, &16=selected media items, &32=selected project markers/regions // RENDER_TAILMS : tail length in ms to render (only used if RENDER_BOUNDSFLAG and RENDER_TAILFLAG are set) // RENDER_ADDTOPROJ : &1=add rendered files to project, &2=do not render files that are likely silent // RENDER_DITHER : &1=dither, &2=noise shaping, &4=dither stems, &8=noise shaping on stems -// RENDER_NORMALIZE: &1=enable, (&14==0)=LUFS-I, (&14==2)=RMS, (&14==4)=peak, (&14==6)=true peak, (&14==8)=LUFS-M max, (&14==10)=LUFS-S max, &32=normalize stems to common gain based on master, &64=enable brickwall limit +// RENDER_NORMALIZE: &1=enable, (&14==0)=LUFS-I, (&14==2)=RMS, (&14==4)=peak, (&14==6)=true peak, (&14==8)=LUFS-M max, (&14==10)=LUFS-S max, &32=normalize stems to common gain based on master, &64=enable brickwall limit, &128=brickwall limit true peak, &256=only normalize files that are too loud, &512=apply fade-in, &1024=apply fade-out // RENDER_NORMALIZE_TARGET: render normalization target as amplitude, so 0.5 means -6.02dB, 0.25 means -12.04dB, etc // RENDER_BRICKWALL: render brickwall limit as amplitude, so 0.5 means -6.02dB, 0.25 means -12.04dB, etc +// RENDER_FADEIN: render fade-in (0.001 means 1 ms, requires RENDER_NORMALIZE&512) +// RENDER_FADEOUT: render fade-out (0.001 means 1 ms, requires RENDER_NORMALIZE&1024) +// RENDER_FADEINSHAPE: render fade-in shape +// RENDER_FADEOUTSHAPE: render fade-out shape // PROJECT_SRATE : samplerate (ignored unless PROJECT_SRATE_USE set) // PROJECT_SRATE_USE : set to 1 if project samplerate is used // @@ -2839,6 +2859,8 @@ REAPERAPI_DEF //============================================== REAPERAPI_DEF //============================================== // GetSetTrackGroupMembership // Gets or modifies the group membership for a track. Returns group state prior to call (each bit represents one of the 32 group numbers). if setmask has bits set, those bits in setvalue will be applied to group. Group can be one of: +// ITEM_EDIT_LEAD +// ITEM_EDIT_FOLLOW // VOLUME_LEAD // VOLUME_FOLLOW // VOLUME_VCA_LEAD @@ -2873,6 +2895,8 @@ REAPERAPI_DEF //============================================== REAPERAPI_DEF //============================================== // GetSetTrackGroupMembershipHigh // Gets or modifies the group membership for a track. Returns group state prior to call (each bit represents one of the high 32 group numbers). if setmask has bits set, those bits in setvalue will be applied to group. Group can be one of: +// ITEM_EDIT_LEAD +// ITEM_EDIT_FOLLOW // VOLUME_LEAD // VOLUME_FOLLOW // VOLUME_VCA_LEAD @@ -3213,7 +3237,7 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_GetTrackMIDILyrics) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // GetTrackMIDILyrics -// Get all MIDI lyrics on the track. Lyrics will be returned as one string with tabs between each word. flag&1: double tabs at the end of each measure and triple tabs when skipping measures, flag&2: each lyric is preceded by its beat position in the project (example with flag=2: "1.1.2\tLyric for measure 1 beat 2\t.1.1\tLyric for measure 2 beat 1 "). See SetTrackMIDILyrics +// Get all MIDI lyrics on the track. Lyrics will be returned as one string with tabs between each word. flag&1: double tabs at the end of each measure and triple tabs when skipping measures, flag&2: each lyric is preceded by its beat position in the project (example with flag=2: "1.1.2\tLyric for measure 1 beat 2\t2.1.1\tLyric for measure 2 beat 1 "). See SetTrackMIDILyrics bool (*GetTrackMIDILyrics)(MediaTrack* track, int flag, char* bufOutWantNeedBig, int* bufOutWantNeedBig_sz); #endif @@ -4129,6 +4153,14 @@ REAPERAPI_DEF //============================================== void (*LICE_SimpleFill)(LICE_IBitmap* dest, int x, int y, LICE_pixel newcolor, LICE_pixel comparemask, LICE_pixel keepmask); #endif +#if defined(REAPERAPI_WANT_LICE_ThickFLine) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// LICE_ThickFLine +// always AA. wid is not affected by scaling (1 is always normal line, 2 is always 2 physical pixels, etc) + + void (*LICE_ThickFLine)(LICE_IBitmap* dest, double x1, double y1, double x2, double y2, LICE_pixel color, float alpha, int mode, int wid); +#endif + #endif // !REAPERAPI_NO_LICE #if defined(REAPERAPI_WANT_LocalizeString) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== @@ -4179,6 +4211,14 @@ REAPERAPI_DEF //============================================== void (*Main_SaveProject)(ReaProject* proj, bool forceSaveAsInOptional); #endif +#if defined(REAPERAPI_WANT_Main_SaveProjectEx) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// Main_SaveProjectEx +// Save the project. options: &1=save selected tracks as track template, &2=include media with track template, &4=include envelopes with track template. See Main_openProject, Main_SaveProject. + + void (*Main_SaveProjectEx)(ReaProject* proj, const char* filename, int options); +#endif + #if defined(REAPERAPI_WANT_Main_UpdateLoopInfo) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // Main_UpdateLoopInfo @@ -4494,6 +4534,14 @@ REAPERAPI_DEF //============================================== bool (*MIDI_GetTrackHash)(MediaTrack* track, bool notesonly, char* hashOut, int hashOut_sz); #endif +#if defined(REAPERAPI_WANT_midi_init) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// midi_init +// Opens MIDI devices as configured in preferences. force_reinit_input and force_reinit_output force a particular device index to close/re-open (pass -1 to not force any devices to reopen). + + void (*midi_init)(int force_reinit_input, int force_reinit_output); +#endif + #if defined(REAPERAPI_WANT_MIDI_InsertCC) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // MIDI_InsertCC @@ -4529,7 +4577,7 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_midi_reinit) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // midi_reinit -// Reset all MIDI devices +// Reset (close and re-open) all MIDI devices void (*midi_reinit)(); #endif @@ -5138,6 +5186,14 @@ REAPERAPI_DEF //============================================== int (*PromptForAction)(int session_mode, int init_id, int section_id); #endif +#if defined(REAPERAPI_WANT_realloc_cmd_clear) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// realloc_cmd_clear +// clears a buffer/buffer-size registration added with realloc_cmd_register_buf, and clears any later registrations, frees any allocated buffers. call after values are read from any registered pointers etc. + + void (*realloc_cmd_clear)(int tok); +#endif + #if defined(REAPERAPI_WANT_realloc_cmd_ptr) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // realloc_cmd_ptr @@ -5146,6 +5202,14 @@ REAPERAPI_DEF //============================================== bool (*realloc_cmd_ptr)(char** ptr, int* ptr_size, int new_size); #endif +#if defined(REAPERAPI_WANT_realloc_cmd_register_buf) || !defined(REAPERAPI_MINIMAL) +REAPERAPI_DEF //============================================== +// realloc_cmd_register_buf +// registers a buffer/buffer-size which may be reallocated by an API (ptr/ptr_size will be updated to the new values). returns a token which should be passed to realloc_cmd_clear after API call and values are read. + + int (*realloc_cmd_register_buf)(char** ptr, int* ptr_size); +#endif + #if defined(REAPERAPI_WANT_ReaperGetPitchShiftAPI) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // ReaperGetPitchShiftAPI @@ -5566,6 +5630,7 @@ REAPERAPI_DEF //============================================== // I_RECARM : int * : record armed, 0=not record armed, 1=record armed // I_RECINPUT : int * : record input, <0=no input. if 4096 set, input is MIDI and low 5 bits represent channel (0=all, 1-16=only chan), next 6 bits represent physical input (63=all, 62=VKB). If 4096 is not set, low 10 bits (0..1023) are input start channel (ReaRoute/Loopback start at 512). If 2048 is set, input is multichannel input (using track channel count), or if 1024 is set, input is stereo input, otherwise input is mono. // I_RECMODE : int * : record mode, 0=input, 1=stereo out, 2=none, 3=stereo out w/latency compensation, 4=midi output, 5=mono out, 6=mono out w/ latency compensation, 7=midi overdub, 8=midi replace +// I_RECMODE_FLAGS : int * : record mode flags, &3=output recording mode (0=post fader, 1=pre-fx, 2=post-fx/pre-fader) // I_RECMON : int * : record monitoring, 0=off, 1=normal, 2=not when playing (tape style) // I_RECMONITEMS : int * : monitor items while recording, 0=off, 1=on // B_AUTO_RECARM : bool * : automatically set record arm when selected (does not immediately affect recarm state, script should set directly if desired) @@ -5593,14 +5658,15 @@ REAPERAPI_DEF //============================================== // D_DUALPANL : double * : dualpan position 1, -1..1, only if I_PANMODE==6 // D_DUALPANR : double * : dualpan position 2, -1..1, only if I_PANMODE==6 // I_PANMODE : int * : pan mode, 0=classic 3.x, 3=new balance, 5=stereo pan, 6=dual pan -// D_PANLAW : double * : pan law of track, <0=project default, 1=+0dB, etc +// D_PANLAW : double * : pan law of track, <0=project default, 0.5=-6dB, 0.707..=-3dB, 1=+0dB, 1.414..=-3dB with gain compensation, 2=-6dB with gain compensation, etc +// I_PANLAW_FLAGS : int * : pan law flags, 0=sine taper, 1=hybrid taper with deprecated behavior when gain compensation enabled, 2=linear taper, 3=hybrid taper // P_ENV:<envchunkname or P_ENV:{GUID... : TrackEnvelope * : (read-only) chunkname can be <VOLENV, <PANENV, etc; GUID is the stringified envelope GUID. // B_SHOWINMIXER : bool * : track control panel visible in mixer (do not use on master track) // B_SHOWINTCP : bool * : track control panel visible in arrange view (do not use on master track) // B_MAINSEND : bool * : track sends audio to parent // C_MAINSEND_OFFS : char * : channel offset of track send to parent // C_MAINSEND_NCH : char * : channel count of track send to parent (0=use all child track channels, 1=use one channel only) -// B_FREEMODE : bool * : track free item positioning enabled (call UpdateTimeline() after changing) +// I_FREEMODE : int * : 1=track free item positioning enabled, 2=track fixed lanes enabled (call UpdateTimeline() after changing) // C_BEATATTACHMODE : char * : track timebase, -1=project default, 0=time, 1=beats (position, length, rate), 2=beats (position only) // F_MCP_FXSEND_SCALE : float * : scale of fx+send area in MCP (0=minimum allowed, 1=maximum allowed) // F_MCP_FXPARM_SCALE : float * : scale of fx parameter area in MCP (0=minimum allowed, 1=maximum allowed) @@ -5721,9 +5787,9 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_SetRegionRenderMatrix) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // SetRegionRenderMatrix -// Add (addorremove > 0) or remove (addorremove < 0) a track from this region when using the region render matrix. +// Add (flag > 0) or remove (flag < 0) a track from this region when using the region render matrix. If adding, flag==2 means force mono, flag==4 means force stereo, flag==N means force N/2 channels. - void (*SetRegionRenderMatrix)(ReaProject* proj, int regionindex, MediaTrack* track, int addorremove); + void (*SetRegionRenderMatrix)(ReaProject* proj, int regionindex, MediaTrack* track, int flag); #endif #if defined(REAPERAPI_WANT_SetRenderLastError) || !defined(REAPERAPI_MINIMAL) @@ -5800,7 +5866,7 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_SetTrackMIDILyrics) || !defined(REAPERAPI_MINIMAL) REAPERAPI_DEF //============================================== // SetTrackMIDILyrics -// Set all MIDI lyrics on the track. Lyrics will be stuffed into any MIDI items found in range. Flag is unused at present. str is passed in as beat position, tab, text, tab (example with flag=2: "1.1.2\tLyric for measure 1 beat 2\t.1.1\tLyric for measure 2 beat 1 "). See GetTrackMIDILyrics +// Set all MIDI lyrics on the track. Lyrics will be stuffed into any MIDI items found in range. Flag is unused at present. str is passed in as beat position, tab, text, tab (example with flag=2: "1.1.2\tLyric for measure 1 beat 2\t2.1.1\tLyric for measure 2 beat 1 "). See GetTrackMIDILyrics bool (*SetTrackMIDILyrics)(MediaTrack* track, int flag, const char* str); #endif @@ -7183,6 +7249,9 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_BypassFxAllTracks) || !defined(REAPERAPI_MINIMAL) {(void**)&BypassFxAllTracks,"BypassFxAllTracks"}, #endif + #if defined(REAPERAPI_WANT_CalcMediaSrcLoudness) || !defined(REAPERAPI_MINIMAL) + {(void**)&CalcMediaSrcLoudness,"CalcMediaSrcLoudness"}, + #endif #if defined(REAPERAPI_WANT_CalculateNormalization) || !defined(REAPERAPI_MINIMAL) {(void**)&CalculateNormalization,"CalculateNormalization"}, #endif @@ -8530,6 +8599,9 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_Main_SaveProject) || !defined(REAPERAPI_MINIMAL) {(void**)&Main_SaveProject,"Main_SaveProject"}, #endif + #if defined(REAPERAPI_WANT_Main_SaveProjectEx) || !defined(REAPERAPI_MINIMAL) + {(void**)&Main_SaveProjectEx,"Main_SaveProjectEx"}, + #endif #if defined(REAPERAPI_WANT_Main_UpdateLoopInfo) || !defined(REAPERAPI_MINIMAL) {(void**)&Main_UpdateLoopInfo,"Main_UpdateLoopInfo"}, #endif @@ -8647,6 +8719,9 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_MIDI_GetTrackHash) || !defined(REAPERAPI_MINIMAL) {(void**)&MIDI_GetTrackHash,"MIDI_GetTrackHash"}, #endif + #if defined(REAPERAPI_WANT_midi_init) || !defined(REAPERAPI_MINIMAL) + {(void**)&midi_init,"midi_init"}, + #endif #if defined(REAPERAPI_WANT_MIDI_InsertCC) || !defined(REAPERAPI_MINIMAL) {(void**)&MIDI_InsertCC,"MIDI_InsertCC"}, #endif @@ -8878,9 +8953,15 @@ REAPERAPI_DEF //============================================== #if defined(REAPERAPI_WANT_PromptForAction) || !defined(REAPERAPI_MINIMAL) {(void**)&PromptForAction,"PromptForAction"}, #endif + #if defined(REAPERAPI_WANT_realloc_cmd_clear) || !defined(REAPERAPI_MINIMAL) + {(void**)&realloc_cmd_clear,"realloc_cmd_clear"}, + #endif #if defined(REAPERAPI_WANT_realloc_cmd_ptr) || !defined(REAPERAPI_MINIMAL) {(void**)&realloc_cmd_ptr,"realloc_cmd_ptr"}, #endif + #if defined(REAPERAPI_WANT_realloc_cmd_register_buf) || !defined(REAPERAPI_MINIMAL) + {(void**)&realloc_cmd_register_buf,"realloc_cmd_register_buf"}, + #endif #if defined(REAPERAPI_WANT_ReaperGetPitchShiftAPI) || !defined(REAPERAPI_MINIMAL) {(void**)&ReaperGetPitchShiftAPI,"ReaperGetPitchShiftAPI"}, #endif diff --git a/Package.distribute b/Package.distribute @@ -3,17 +3,20 @@ xmlns:tar="attrib://tar" xmlns:zip="attrib://7z"> + <!-- This file is located with the BuildTools in the Configs folder. --> + <import path="CommonConfigurations.distribute" /> + <tasks> - <task name="ReaperWwiseTransfer.Mac" export-path="${MAC_NAS_PATH}/ReaperWwiseTransfer/${VERSION}/${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}/Artifacts" zip:archive-name="ReaperWwiseTransfer_MacOS_${CONFIG}"> + <task name="ReaperWwiseTransfer.Mac" export-path="${BUILDS_LOCATION}/ReaperWwiseTransfer/${VERSION}/${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}/Artifacts" zip:archive-name="ReaperWwiseTransfer_MacOS_${CONFIG}"> <includegroup>Binary</includegroup> </task> - <task name="ReaperWwiseTransfer.Windows" export-path="${WINDOWS_NAS_PATH}\ReaperWwiseTransfer\${VERSION}\${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}\Artifacts" zip:archive-name="ReaperWwiseTransfer_Windows_${CONFIG}"> + <task name="ReaperWwiseTransfer.Windows" export-path="${BUILDS_LOCATION}\ReaperWwiseTransfer\${VERSION}\${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}\Artifacts" zip:archive-name="ReaperWwiseTransfer_Windows_${CONFIG}"> <includegroup>Binary</includegroup> </task> - <task name="ReaperWwiseTransferBinaryBundles.Windows" export-path="${WINDOWS_NAS_PATH}\ReaperWwiseTransfer\${VERSION}\${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}\Bundles" zip:archive-name="ReaperWwiseTransfer_${VERSION}_Build${BUILD_NUMBER}_Windows"> + <task name="ReaperWwiseTransferBinaryBundles.Windows" export-path="${BUILDS_LOCATION}\ReaperWwiseTransfer\${VERSION}\${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}\Bundles" zip:archive-name="ReaperWwiseTransfer_${VERSION}_Build${BUILD_NUMBER}_Windows"> <includegroup>BinaryBundlesWindows</includegroup> </task> - <task name="ReaperWwiseTransferBinaryBundles.Mac" export-path="${MAC_NAS_PATH}/ReaperWwiseTransfer/${VERSION}/${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}/Bundles" zip:archive-name="ReaperWwiseTransfer_${VERSION}_Build${BUILD_NUMBER}_MacOS"> + <task name="ReaperWwiseTransferBinaryBundles.Mac" export-path="${BUILDS_LOCATION}/ReaperWwiseTransfer/${VERSION}/${WWISE_VERSION}.${REAPER_WWISE_TRANSFER_VERSION}/Bundles" zip:archive-name="ReaperWwiseTransfer_${VERSION}_Build${BUILD_NUMBER}_MacOS"> <includegroup>BinaryBundlesMac</includegroup> </task> </tasks> diff --git a/Package.instrument b/Package.instrument @@ -60,8 +60,6 @@ <fail message="Requires the 'BUILD_NUMBER' property to be set." if="${BUILD_NUMBER is None}" /> <fail message="Requires the 'WWISE_VERSION' property to be set." if="${WWISE_VERSION is None}" /> <fail message="Requires the 'VERSION' property to be set." if="${VERSION is None}" /> - <fail message="Requires the 'MAC_NAS_PATH' property to be set." if="${MAC_NAS_PATH is None}" /> - <fail message="Requires the 'WINDOWS_NAS_PATH' property to be set." if="${WINDOWS_NAS_PATH is None}" /> <property name="REAPER_WWISE_TRANSFER_VERSION" value="${WWISE_BUILD_NUMBER}.${BUILD_NUMBER}" /> @@ -83,8 +81,6 @@ <fail message="Requires the 'BUILD_NUMBER' property to be set." if="${BUILD_NUMBER is None}" /> <fail message="Requires the 'WWISE_VERSION' property to be set." if="${WWISE_VERSION is None}" /> <fail message="Requires the 'VERSION' property to be set." if="${VERSION is None}" /> - <fail message="Requires the 'MAC_NAS_PATH' property to be set." if="${MAC_NAS_PATH is None}" /> - <fail message="Requires the 'WINDOWS_NAS_PATH' property to be set." if="${WINDOWS_NAS_PATH is None}" /> <property name="REAPER_WWISE_TRANSFER_VERSION" value="${WWISE_BUILD_NUMBER}.${BUILD_NUMBER}" /> <property name="CONFIG" value="Release" /> diff --git a/Readme.md b/Readme.md @@ -19,6 +19,7 @@ ReaWwise is a REAPER extension that sound designers can use to transfer audio fi - Mac ### Manual installation + 1. Go to the the [releases page](https://github.com/audiokinetic/ReaWwise/releases), then download the extension file (reaper_reawwise.dll for Windows or reaper_reawwise.dylib for Mac). 2. Copy the extension to the REAPER [UserPlugins directory](#userplugins-directory). @@ -26,13 +27,19 @@ ReaWwise is a REAPER extension that sound designers can use to transfer audio fi 3. If REAPER is running, restart the application. The extension is available in the Extensions menu. ### Installing ReaWwise through ReaPack -1. In the REAPER menu, go to **Extensions** > **ReaPack** > **Browse Packages**. A dialog opens, and displays a list of packages available for installation. +#### Importing the Audiokinetic Reaper Tools repository +1. In the REAPER menu, go to **Extensions** > **ReaPack** > **Import Repositories**. + +2. In the dialog, insert the url of Audiokinetic's Reaper Tools repository (https://github.com/Audiokinetic/Reaper-Tools/raw/main/index.xml) and click Ok. + +#### Installing ReaWwise +1. In the REAPER menu, go to **Extensions** > **ReaPack** > **Browse Packages**. -2. Right-click on the ReaWwise package and select the version you want to install. +2. Right-click the ReaWwise package and select the version that you want to install. -3. Click **Apply** in the bottom-right corner of the dialog. The ReaWwise package is downloaded and moved to the UserPlugins directory. +3. Click **Apply** in the lower right corner of the dialog to complete the installation. -4. After the plugin installation is complete, restart REAPER. The extension is now available in the **Extensions** menu. +4. Restart REAPER to use the latest installed version of ReaWwise. ## Building from Source @@ -57,7 +64,7 @@ After the binary is built, move it to the REAPER [UserPlugins directory](#userpl 1. Open your REAPER and Wwise projects. 2. Configure your REAPER render settings appropriately. The render settings determine which files will be transferred to Wwise. 3. Open ReaWwise and configure settings such as the Originals Subfolder (optional), Import Destination, Wwise Structures, and so on. -4. Preview the audio files and Wwise objects to be transfered in the Preview Panel. +4. Preview the audio files and Wwise objects to be transferred in the Preview Panel. 4. Click **Transfer To Wwise** to transfer the audio files and create the corresponding Wwise objects. ## UserPlugins Directory @@ -75,7 +82,7 @@ The repository is not open to pull request but in the case of a bug report, bugf Feature requests can also be submitted to the [feature request section](https://www.audiokinetic.com/qa/feature-requests/) of Audiokinetic's Community Q&A. Use ReaWwise as the Category when submitting a question. ## Legal -Copyright © 2020 [Audiokinetic Inc.](https://audiokinetic.com) All rights reserved. +Copyright © 2022 [Audiokinetic Inc.](https://audiokinetic.com) All rights reserved. ## Acknowledgements Inspired by the work of [Karl Davis](https://github.com/karltechno) \ No newline at end of file diff --git a/src/extension/CMakeLists.txt b/src/extension/CMakeLists.txt @@ -42,6 +42,7 @@ if(NOT DEFINED ENV{BUILD_NUMBER}) endif() endif() + set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME reaper_reawwise) @@ -73,22 +74,18 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE REAPERAPI_MINIMAL REAPERAPI_WANT_GetMainHwnd - REAPERAPI_WANT_ShowConsoleMsg REAPERAPI_WANT_AddExtensionsMainMenu REAPERAPI_WANT_EnumProjects REAPERAPI_WANT_GetSetProjectInfo_String - REAPERAPI_WANT_GetProjectName REAPERAPI_WANT_ResolveRenderPattern - REAPERAPI_WANT_EnumProjectMarkers2 - REAPERAPI_WANT_EnumRegionRenderMatrix - REAPERAPI_WANT_GetParentTrack - REAPERAPI_WANT_GetTrackName REAPERAPI_WANT_Main_OnCommand - REAPERAPI_WANT_GetProjectPathEx - REAPERAPI_WANT_ShowMessageBox REAPERAPI_WANT_GetProjExtState REAPERAPI_WANT_SetProjExtState REAPERAPI_WANT_MarkProjectDirty + REAPERAPI_WANT_realloc_cmd_register_buf + REAPERAPI_WANT_realloc_cmd_clear + REAPERAPI_WANT_GetProjectStateChangeCount + REAPERAPI_WANT_GetSetProjectInfo JUCE_APPLICATION_NAME_STRING="${PROJECT_NAME}" JUCE_STANDALONE_APPLICATION=0 ) diff --git a/src/extension/Extension.cpp b/src/extension/Extension.cpp @@ -1,9 +1,8 @@ #define REAPERAPI_IMPLEMENT +#include "Core/WaapiClient.h" #include "ExtensionWindow.h" #include "ReaperContext.h" - -#include "Core/WaapiClient.h" #include "Theme/CustomLookAndFeel.h" #include <JSONHelpers.h> @@ -20,8 +19,11 @@ namespace AK::ReaWwise { static bool juceInitialised = false; + static constexpr int defaultBufferSize = 4096; + static constexpr int largeBufferSize = 4 * 1024 * 1024; static std::unique_ptr<ExtensionWindow> mainWindow; static std::unique_ptr<ReaperContext> reaperContext; + static std::unique_ptr<ReaperPluginInterface> reaperPluginInterface; static std::string returnString; static std::string emptyReturnString; static double returnDouble; @@ -54,10 +56,10 @@ namespace AK::ReaWwise { mainWindow = std::make_unique<ExtensionWindow>(*reaperContext); #ifdef WIN32 - mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), reaperContext->getMainHwnd()); + mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), reaperPluginInterface->getMainHwnd()); #else mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), 0); - MacHelpers::makeWindowFloatingPanel(dynamic_cast<juce::Component*>(mainWindow.get())); + // MacHelpers::makeWindowFloatingPanel(dynamic_cast<juce::Component*>(mainWindow.get())); #endif } @@ -707,43 +709,193 @@ namespace AK::ReaWwise static int initialize(reaper_plugin_info_t* pluginInfo) { - reaperContext = std::make_unique<ReaperContext>(pluginInfo); + class ReaperPluginImplementation : public ReaperPluginInterface + { + public: + ReaperPluginImplementation(reaper_plugin_info_t* pluginInfo) + : pluginInfo(pluginInfo) + { + _getMainHwnd = decltype(GetMainHwnd)(pluginInfo->GetFunc("GetMainHwnd")); + _addExtensionsMainMenu = decltype(AddExtensionsMainMenu)(pluginInfo->GetFunc("AddExtensionsMainMenu")); + _enumProjects = decltype(EnumProjects)(pluginInfo->GetFunc("EnumProjects")); + _getSetProjectInfo_String = decltype(GetSetProjectInfo_String)(pluginInfo->GetFunc("GetSetProjectInfo_String")); + _resolveRenderPattern = decltype(ResolveRenderPattern)(pluginInfo->GetFunc("ResolveRenderPattern")); + _main_OnCommand = decltype(Main_OnCommand)(pluginInfo->GetFunc("Main_OnCommand")); + _getProjExtState = decltype(GetProjExtState)(pluginInfo->GetFunc("GetProjExtState")); + _setProjExtState = decltype(SetProjExtState)(pluginInfo->GetFunc("SetProjExtState")); + _markProjectDirty = decltype(MarkProjectDirty)(pluginInfo->GetFunc("MarkProjectDirty")); + _getProjectStateChangeCount = decltype(GetProjectStateChangeCount)(pluginInfo->GetFunc("GetProjectStateChangeCount")); + _getSetProjectInfo = decltype(GetSetProjectInfo)(pluginInfo->GetFunc("GetSetProjectInfo")); + _realloc_cmd_register_buf = decltype(realloc_cmd_register_buf)(pluginInfo->GetFunc("realloc_cmd_register_buf")); + _realloc_cmd_clear = decltype(realloc_cmd_clear)(pluginInfo->GetFunc("realloc_cmd_clear")); + } + + ~ReaperPluginImplementation() override = default; + + int getCallerVersion() const override + { + return pluginInfo->caller_version; + } + + int registerFunction(const char* name, void* infoStruct) const override + { + return pluginInfo->Register(name, infoStruct); + } + + bool isValid() const override + { + if(_getMainHwnd && + _addExtensionsMainMenu && + _enumProjects && + _getSetProjectInfo_String && + _resolveRenderPattern && + _main_OnCommand && + _getProjExtState && + _setProjExtState && + _markProjectDirty && + _getProjectStateChangeCount && + _getSetProjectInfo) + return true; + + return false; + } + + void* getMainHwnd() override + { + return _getMainHwnd(); + } + + bool addExtensionsMainMenu() override + { + return _addExtensionsMainMenu(); + } + + ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) override + { + return _enumProjects(idx, projfnOutOptional, projfnOutOptional_sz); + } + + juce::String getProjectString(ReaProject* proj, const char* key) override + { + juce::String projectString; + + if (realloc_cmd_register_buf && realloc_cmd_clear) + { + // For REAPER 6.68+ + char buffer[defaultBufferSize]; + char* bufferPtr = buffer; + + int bufferSize = (int)sizeof(buffer); + + int token = realloc_cmd_register_buf(&bufferPtr, &bufferSize); + + if (_getSetProjectInfo_String(proj, key, bufferPtr, false)) + projectString = juce::String(bufferPtr, bufferSize); + + realloc_cmd_clear(token); + } + else + { + static std::string buffer(largeBufferSize, '\0'); + + if (_getSetProjectInfo_String(proj, key, &buffer[0], false)) + projectString = buffer; + } + + return projectString; + } + + int resolveRenderPattern(ReaProject* project, const char* path, const char* pattern, char* targets, int targets_sz) override + { + return _resolveRenderPattern(project, path, pattern, targets, targets_sz); + } + + void main_OnCommand(int command, int flag) override + { + _main_OnCommand(command, flag); + } + + int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) override + { + return _getProjExtState(proj, extname, key, valOutNeedBig, valOutNeedBig_sz); + } + + int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) override + { + return _setProjExtState(proj, extname, key, value); + } + + void markProjectDirty(ReaProject* proj) override + { + return _markProjectDirty(proj); + } + + int getProjectStateChangeCount(ReaProject* proj) override + { + return _getProjectStateChangeCount(proj); + } + + double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) override + { + return _getSetProjectInfo(proj, desc, value, is_set); + } + + private: + reaper_plugin_info_t* pluginInfo; + + decltype(GetMainHwnd) _getMainHwnd; + decltype(AddExtensionsMainMenu) _addExtensionsMainMenu; + decltype(EnumProjects) _enumProjects; + decltype(GetSetProjectInfo_String) _getSetProjectInfo_String; + decltype(ResolveRenderPattern) _resolveRenderPattern; + decltype(Main_OnCommand) _main_OnCommand; + decltype(GetProjExtState) _getProjExtState; + decltype(SetProjExtState) _setProjExtState; + decltype(MarkProjectDirty) _markProjectDirty; + decltype(GetProjectStateChangeCount) _getProjectStateChangeCount; + decltype(GetSetProjectInfo) _getSetProjectInfo; + decltype(realloc_cmd_register_buf) _realloc_cmd_register_buf; + decltype(realloc_cmd_clear) _realloc_cmd_clear; + }; + + reaperPluginInterface = std::make_unique<ReaperPluginImplementation>(pluginInfo); + reaperContext = std::make_unique<ReaperContext>(*reaperPluginInterface); // Should actually report errors to the user somehow - if(reaperContext->callerVersion() != REAPER_PLUGIN_VERSION) + if(reaperPluginInterface->getCallerVersion() != REAPER_PLUGIN_VERSION) { return 0; } // Checks that all function pointers needed from reaper are valid - if(!reaperContext->isValid()) + if(!reaperPluginInterface->isValid()) { return 0; } - openReaperWwiseTransferCommandId = reaperContext->registerFunction("command_id", (void*)"openReaperWwiseTransferCommand"); + openReaperWwiseTransferCommandId = reaperPluginInterface->registerFunction("command_id", (void*)"openReaperWwiseTransferCommand"); if(!openReaperWwiseTransferCommandId) { return 0; } - if(!reaperContext->registerFunction("hookcommand", (void*)onHookCommand)) + if(!reaperPluginInterface->registerFunction("hookcommand", (void*)onHookCommand)) { return 0; } - if(!reaperContext->registerFunction("hookcustommenu", (void*)onHookCustomMenu)) + if(!reaperPluginInterface->registerFunction("hookcustommenu", (void*)onHookCustomMenu)) { return 0; } - reaperContext->addExtensionsMainMenu(); + reaperPluginInterface->addExtensionsMainMenu(); for(const auto& apiFunctionDefinition : Scripting::apiFunctionDefinitions) { - reaperContext->registerFunction(apiFunctionDefinition.Api, (void*)apiFunctionDefinition.FunctionPointer); - reaperContext->registerFunction(apiFunctionDefinition.ApiVarArg, (void*)apiFunctionDefinition.FunctionPointerVarArg); - reaperContext->registerFunction(apiFunctionDefinition.ApiDef, (void*)apiFunctionDefinition.FunctionSignature); + reaperPluginInterface->registerFunction(apiFunctionDefinition.Api, (void*)apiFunctionDefinition.FunctionPointer); + reaperPluginInterface->registerFunction(apiFunctionDefinition.ApiVarArg, (void*)apiFunctionDefinition.FunctionPointerVarArg); + reaperPluginInterface->registerFunction(apiFunctionDefinition.ApiDef, (void*)apiFunctionDefinition.FunctionSignature); } return 1; diff --git a/src/extension/ExtensionWindow.cpp b/src/extension/ExtensionWindow.cpp @@ -1,5 +1,4 @@ #include "ExtensionWindow.h" - #include "UI/MainComponent.h" #include <limits> diff --git a/src/extension/ReaperContext.cpp b/src/extension/ReaperContext.cpp @@ -3,12 +3,6 @@ #include "Helpers/WwiseHelper.h" #include "Model/Wwise.h" -#include <algorithm> -#include <cstdlib> -#include <map> -#include <memory> -#include <optional> - namespace AK::ReaWwise { namespace ReaperContextConstants @@ -17,34 +11,23 @@ namespace AK::ReaWwise const juce::String stateSizeKey = "stateSize"; const juce::String stateKey = "state"; const juce::String applicationKey = "ReaWwise"; + const juce::String defaultRenderPattern = "untitled"; } // namespace ReaperContextConstants - ReaperContext::ReaperContext(reaper_plugin_info_t* pluginInfo) - : pluginInfo(pluginInfo) + ReaperContext::ReaperContext(ReaperPluginInterface& pluginInfo) + : reaperPluginInterface(pluginInfo) , defaultRenderDirectory(juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("REAPER Media")) - , defaultRenderPattern("untitled") { - getMainHwnd = decltype(GetMainHwnd)(pluginInfo->GetFunc("GetMainHwnd")); - showConsoleMsg = decltype(ShowConsoleMsg)(pluginInfo->GetFunc("ShowConsoleMsg")); - addExtensionsMainMenu = decltype(AddExtensionsMainMenu)(pluginInfo->GetFunc("AddExtensionsMainMenu")); - enumProjects = decltype(EnumProjects)(pluginInfo->GetFunc("EnumProjects")); - getSetProjectInfo_String = decltype(GetSetProjectInfo_String)(pluginInfo->GetFunc("GetSetProjectInfo_String")); - getProjectName = decltype(GetProjectName)(pluginInfo->GetFunc("GetProjectName")); - resolveRenderPattern = decltype(ResolveRenderPattern)(pluginInfo->GetFunc("ResolveRenderPattern")); - enumProjectMarkers2 = decltype(EnumProjectMarkers2)(pluginInfo->GetFunc("EnumProjectMarkers2")); - enumRegionRenderMatrix = decltype(EnumRegionRenderMatrix)(pluginInfo->GetFunc("EnumRegionRenderMatrix")); - getParentTrack = decltype(GetParentTrack)(pluginInfo->GetFunc("GetParentTrack")); - getTrackName = decltype(GetTrackName)(pluginInfo->GetFunc("GetTrackName")); - main_OnCommand = decltype(Main_OnCommand)(pluginInfo->GetFunc("Main_OnCommand")); - getProjectPathEx = decltype(GetProjectPathEx)(pluginInfo->GetFunc("GetProjectPathEx")); - showMessageBox = decltype(ShowMessageBox)(pluginInfo->GetFunc("ShowMessageBox")); - getProjExtState = decltype(GetProjExtState)(pluginInfo->GetFunc("GetProjExtState")); - setProjExtState = decltype(SetProjExtState)(pluginInfo->GetFunc("SetProjExtState")); - markProjectDirty = decltype(MarkProjectDirty)(pluginInfo->GetFunc("MarkProjectDirty")); + } + + ReaperContext::~ReaperContext() + { } juce::String ReaperContext::getSessionName() { + juce::ScopedLock lock{apiAccess}; + auto projectInfo = getProjectInfo(); return projectInfo.projectPath; } @@ -53,14 +36,16 @@ namespace AK::ReaWwise { using namespace ReaperContextConstants; + juce::ScopedLock lock{apiAccess}; + auto projectInfo = getProjectInfo(); const auto applicationStateString = applicationState.toXmlString(); const auto applicationStateStringSize = juce::String(applicationStateString.getNumBytesAsUTF8()); - if(setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), applicationStateStringSize.toUTF8()) && - setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), applicationStateString.toUTF8())) + if(reaperPluginInterface.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), applicationStateStringSize.toUTF8()) && + reaperPluginInterface.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), applicationStateString.toUTF8())) { - markProjectDirty(projectInfo.projectReference); + reaperPluginInterface.markProjectDirty(projectInfo.projectReference); return true; } @@ -71,32 +56,24 @@ namespace AK::ReaWwise { using namespace ReaperContextConstants; + juce::ScopedLock lock{apiAccess}; + auto projectInfo = getProjectInfo(); std::string buffer(defaultBufferSize, '\0'); - getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), &buffer[0], buffer.size()); + reaperPluginInterface.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), &buffer[0], buffer.size()); const auto stateSize = std::strtoll(&buffer[0], nullptr, 10); if(stateSize == 0) return {}; buffer.resize(stateSize); - if(getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), &buffer[0], buffer.size())) + if(reaperPluginInterface.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), &buffer[0], buffer.size())) return juce::ValueTree::fromXml(buffer); return {}; } - int ReaperContext::callerVersion() - { - return pluginInfo->caller_version; - } - - int ReaperContext::registerFunction(const char* command, void* function) - { - return pluginInfo->Register(command, function); - } - std::vector<juce::String> ReaperContext::splitDoubleNullTerminatedString(const char* buffer) { std::vector<juce::String> result; @@ -109,37 +86,15 @@ namespace AK::ReaWwise return result; } - bool ReaperContext::isValid() - { - if(getMainHwnd && - showConsoleMsg && - addExtensionsMainMenu && - enumProjects && - getSetProjectInfo_String && - getProjectName && - resolveRenderPattern && - enumProjectMarkers2 && - enumRegionRenderMatrix && - getParentTrack && - getTrackName && - main_OnCommand && - getProjectPathEx && - showMessageBox && - getProjExtState && - setProjExtState && - markProjectDirty) - return true; - - return false; - } - void ReaperContext::renderItems() { - main_OnCommand(42230, 0); + reaperPluginInterface.main_OnCommand(42230, 0); } std::vector<WwiseTransfer::Import::Item> ReaperContext::getItemsForImport(const WwiseTransfer::Import::Options& options) { + juce::ScopedLock lock{apiAccess}; + std::vector<WwiseTransfer::Import::Item> importItems; auto importItemsForPreview = getItemsForPreview(options); @@ -147,28 +102,10 @@ namespace AK::ReaWwise return importItems; auto projectInfo = getProjectInfo(); - - // In the following section, we retrieve render stats from reaper. We need to estimate the size of the buffer so that - // no heap corruption occurs when reaper tries to write to the buffer we give it. Unfortunately, there is no way - // for us to know the exact size of this buffer should be. And, it seems like reaper blindly writes to this buffer - // as we can not send it a buffer size. - - // Assuming that volume levels are in [-1000, 1000], this is the max length for an entry for one render would be: - // FILE:{audioFilePath};PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000; - // This is roughly 105 chars + the audioFileName - // Lets use 150 for extra chars just to leave some room for error. - const int extra = 150; - - juce::String renderDirectory = getProjectString(projectInfo.projectReference, "RENDER_FILE"); - - int totalBufferSize = (renderDirectory.length() + projectInfo.projectPath.length() + extra) * static_cast<int>(importItemsForPreview.size()); - for(const auto& item : importItemsForPreview) - { - totalBufferSize += item.audioFilePath.length(); - } + const auto renderDirectory = getRenderDirectory(projectInfo); juce::StringArray temp; - juce::String renderStats = getProjectString(projectInfo.projectReference, "RENDER_STATS", totalBufferSize); + juce::String renderStats = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_STATS"); temp.addTokens(renderStats, ";", ""); juce::StringArray finalRenderPaths; @@ -190,89 +127,70 @@ namespace AK::ReaWwise std::vector<WwiseTransfer::Import::PreviewItem> ReaperContext::getItemsForPreview(const WwiseTransfer::Import::Options& options) { + juce::ScopedLock lock{apiAccess}; + auto projectInfo = getProjectInfo(); - auto renderDirectoryPath = getProjectString(projectInfo.projectReference, "RENDER_FILE"); - auto renderPattern = getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); + const auto renderDirectory = getRenderDirectory(projectInfo); + const auto originalsSubfolderPathPart = options.originalsSubfolder + juce::File::getSeparatorString(); - // There are several scenarios where the render pattern could be empty - // 1. When the project hasn't been saved (reaper uses "untitled") - // 2. When the project has been saved (reaper uses the project name) - if(renderPattern.isEmpty()) - { - if(projectInfo.projectPath.isEmpty()) - { - renderPattern = defaultRenderPattern; - } - else - { - renderPattern = projectInfo.projectName; - } - } + const auto resolvedOriginalsSubfolder = getItemListFromRenderPattern(projectInfo.projectReference, originalsSubfolderPathPart, false); - juce::File renderDirectory; - if(juce::File::isAbsolutePath(renderDirectoryPath)) - { - renderDirectory = juce::File(renderDirectoryPath); - } - else if(projectInfo.projectPath.isNotEmpty()) - { - renderDirectory = juce::File(projectInfo.projectPath).getParentDirectory().getChildFile(renderDirectoryPath); - } - else - { - // If the project wasnt saved, reaper uses a general render directory - renderDirectory = defaultRenderDirectory.getChildFile(renderDirectoryPath); - } + juce::StringArray renderTargets; + juce::String renderTargetsString = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_TARGETS"); + renderTargets.addTokens(renderTargetsString, ";", ""); - const auto originalsSubfolderPathPart = options.originalsSubfolder.isNotEmpty() ? options.originalsSubfolder + juce::File::getSeparatorString() : ""; - const auto resolvedRenderPaths = getItemListFromRenderPattern(projectInfo.projectReference, renderPattern, false); - const auto resolvedRenderPathsWithOriginalsSubfolder = getItemListFromRenderPattern(projectInfo.projectReference, originalsSubfolderPathPart + renderPattern, false); - if(resolvedRenderPaths.size() != resolvedRenderPathsWithOriginalsSubfolder.size()) + if(renderTargets.size() != resolvedOriginalsSubfolder.size()) { - juce::Logger::writeToLog("Reaper: Mismatch between resolvedRenderPaths and resolvedRenderPathsWithOriginalsSubfolder"); + juce::Logger::writeToLog("Reaper: Mismatch between renderTargets and resolvedOriginalsSubfolder"); return {}; } - const auto resolvePattern = options.importDestination + options.hierarchyMappingPath; - std::vector<juce::String> resolvedObjectPaths = getItemListFromRenderPattern(projectInfo.projectReference, resolvePattern, false); - if(resolvedObjectPaths.size() != resolvedRenderPaths.size() || resolvedObjectPaths.size() != resolvedRenderPathsWithOriginalsSubfolder.size()) + const auto objectPathsPattern = options.importDestination + options.hierarchyMappingPath; + std::vector<juce::String> resolvedObjectPaths = getItemListFromRenderPattern(projectInfo.projectReference, objectPathsPattern, false); + + if(resolvedObjectPaths.size() != renderTargets.size() || resolvedObjectPaths.size() != resolvedOriginalsSubfolder.size()) { - juce::Logger::writeToLog("Reaper: Mismatch between resolvedObjectPaths, resolvedRenderPaths and resolvedRenderPathsWithOriginalsSubfolder"); + juce::Logger::writeToLog("Reaper: Mismatch between resolvedObjectPaths, renderTargets and resolvedOriginalsSubfolder"); return {}; } std::vector<WwiseTransfer::Import::PreviewItem> importItems; for(int i = 0; i < resolvedObjectPaths.size(); ++i) { - const auto objectPath = resolvedObjectPaths[i].upToLastOccurrenceOf(".", false, false); - const auto resolvedRenderPath = renderDirectory.getChildFile(resolvedRenderPaths[i]).getFullPathName(); - const auto resolvedRenderPathWithOriginalsSubfolder = renderDirectory.getChildFile(resolvedRenderPathsWithOriginalsSubfolder[i]); - const auto parentDirectory = resolvedRenderPathWithOriginalsSubfolder.getParentDirectory().getRelativePathFrom(renderDirectory); - const auto originalsSubfolder = parentDirectory == "." ? "" : parentDirectory; - importItems.push_back({objectPath, originalsSubfolder, resolvedRenderPath}); - } + const auto& objectPath = resolvedObjectPaths[i].upToLastOccurrenceOf(".", false, false); + const auto& renderTarget = renderTargets[i]; - return importItems; - } + // Get the parent of the render target, as a relative path against the render directory + // We want to preserve this hierarchy in the wwise originals folder + juce::String relativeParentDir; + if(juce::File(renderTarget).getParentDirectory() != renderDirectory) + relativeParentDir = juce::File(renderTarget).getRelativePathFrom(renderDirectory).upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true); - juce::String ReaperContext::getProjectString(ReaProject* project, const char* key, int bufferSize) - { - // getSetProjectInfo_String is unsafe since we cannot specify the size of the buffer that it must fill. - // There are a few cases where we are unable to predict the size required, so we use a large value and hope for the best. - std::string buffer((std::max)(bufferSize, ReaperContextConstants::defaultBufferSize), '\0'); - if(!getSetProjectInfo_String(project, key, &buffer[0], false)) - return {}; + // Reaper may append a differentiator at the end of a path during pattern resolving. We don't care about it. + auto originalsSubfolder = resolvedOriginalsSubfolder[i].upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true); + + // The originalsSubfolder is a combination of what the user inputs in the gui (resolvedOriginalsSubfolder) and the directory structure under the render directory. + if(relativeParentDir.isNotEmpty()) + { + if(originalsSubfolder.isNotEmpty()) + originalsSubfolder << juce::File::getSeparatorString(); + + originalsSubfolder << relativeParentDir; + } - return buffer; + importItems.push_back({objectPath, originalsSubfolder, renderTarget}); + } + + return importItems; } - ProjectInfo ReaperContext::getProjectInfo() + ReaperContext::ProjectInfo ReaperContext::getProjectInfo() const { std::string buffer(ReaperContextConstants::defaultBufferSize, '\0'); // The buffer sent to enumProjects will contain the project path. - auto projectReference = enumProjects(-1, &buffer[0], buffer.size()); + auto projectReference = reaperPluginInterface.enumProjects(-1, &buffer[0], buffer.size()); if(!projectReference || buffer.empty()) return {}; @@ -285,16 +203,83 @@ namespace AK::ReaWwise return { projectReference, projectFile.getFileNameWithoutExtension(), - projectFile.getFullPathName() - }; + projectFile.getFullPathName()}; + } + + juce::String ReaperContext::getRenderPattern(const ReaperContext::ProjectInfo& projectInfo) const + { + // There are several scenarios where the render pattern could be empty + // 1. When the project hasn't been saved (reaper uses "untitled") + // 2. When the project has been saved (reaper uses the project name) + auto renderPattern = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); + if(renderPattern.isNotEmpty()) + return renderPattern; + + if(projectInfo.projectPath.isEmpty()) + return ReaperContextConstants::defaultRenderPattern; + + return projectInfo.projectName; + } + + juce::File ReaperContext::getRenderDirectory(const ReaperContext::ProjectInfo& projectInfo) const + { + auto renderDirectoryPath = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_FILE"); + if(juce::File::isAbsolutePath(renderDirectoryPath)) + return juce::File(renderDirectoryPath); + + if(projectInfo.projectPath.isNotEmpty()) + return juce::File(projectInfo.projectPath).getParentDirectory().getChildFile(renderDirectoryPath); + + // If the project wasnt saved, reaper uses a general render directory + return defaultRenderDirectory.getChildFile(renderDirectoryPath); + } + + bool ReaperContext::sessionChanged() + { + auto sessionChanged = false; + + auto projectInfo = getProjectInfo(); + + auto projectStateCount = reaperPluginInterface.getProjectStateChangeCount(projectInfo.projectReference); + auto renderSource = reaperPluginInterface.getSetProjectInfo(projectInfo.projectReference, "RENDER_SETTINGS", 0, false); + auto renderBounds = reaperPluginInterface.getSetProjectInfo(projectInfo.projectReference, "RENDER_BOUNDSFLAG", 0, false); + auto renderFile = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_FILE"); + auto renderPattern = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); + + if(projectStateCount != stateInfo.projectStateCount || + renderSource != stateInfo.renderSource || + renderBounds != stateInfo.renderBounds || + renderFile != stateInfo.renderFile || + renderPattern != stateInfo.renderPattern) + { + sessionChanged = true; + } + + stateInfo = { + projectStateCount, + renderSource, + renderBounds, + renderFile, + renderPattern}; + + return sessionChanged; } std::vector<juce::String> ReaperContext::getItemListFromRenderPattern(ReaProject* project, const juce::String& pattern, bool suppressIllegalPaths) { - int bufferLength = resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), nullptr, 0); + const int bufferLength = reaperPluginInterface.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), nullptr, 0); std::string buffer(bufferLength, '\0'); - resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), &buffer[0], bufferLength); + const int newBufferLength = reaperPluginInterface.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), &buffer[0], bufferLength); + if(newBufferLength > bufferLength) + { + // It is possible the resolved render pattern changes between the two calls to resolveRenderPattern. + // For example, a track that is set to render can be unmuted between the two calls which will result in a bigger buffer needed. + // In that case we just return nothing and the next call will be good. + juce::Logger::writeToLog("Reaper: Mismatch between calls to resolveRenderPattern"); + return {}; + } + return splitDoubleNullTerminatedString(&buffer[0]); } } // namespace AK::ReaWwise diff --git a/src/extension/ReaperContext.h b/src/extension/ReaperContext.h @@ -3,23 +3,39 @@ #include "Core/DawContext.h" #include "Model/Import.h" -#include <reaper_plugin_functions.h> - +class ReaProject; namespace AK::ReaWwise { - struct ProjectInfo + class ReaperPluginInterface { - ReaProject* projectReference; - juce::String projectName; - juce::String projectPath; + public: + virtual ~ReaperPluginInterface() = default; + + virtual int getCallerVersion() const = 0; + virtual int registerFunction(const char* name, void* infoStruct) const = 0; + virtual bool isValid() const = 0; + + virtual void* getMainHwnd() = 0; + virtual bool addExtensionsMainMenu() = 0; + virtual ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) = 0; + virtual juce::String getProjectString(ReaProject* project, const char* key) = 0; + virtual int resolveRenderPattern(ReaProject* proj, const char* path, const char* pattern, char* targets, int targets_sz) = 0; + virtual void main_OnCommand(int command, int flag) = 0; + virtual int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) = 0; + virtual int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) = 0; + virtual void markProjectDirty(ReaProject* proj) = 0; + virtual int getProjectStateChangeCount(ReaProject* proj) = 0; + virtual double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) = 0; }; class ReaperContext : public WwiseTransfer::DawContext { public: - ReaperContext(reaper_plugin_info_t* pluginInfo); + ReaperContext(ReaperPluginInterface& pluginInfo); + ~ReaperContext() override; + bool sessionChanged() override; juce::String getSessionName() override; bool saveState(juce::ValueTree applicationState) override; juce::ValueTree retrieveState() override; @@ -27,37 +43,35 @@ namespace AK::ReaWwise std::vector<WwiseTransfer::Import::PreviewItem> getItemsForPreview(const WwiseTransfer::Import::Options& options) override; std::vector<WwiseTransfer::Import::Item> getItemsForImport(const WwiseTransfer::Import::Options& options) override; - // Reaper api for public use - bool isValid(); - int callerVersion(); - decltype(GetMainHwnd) getMainHwnd; - decltype(ShowConsoleMsg) showConsoleMsg; - decltype(AddExtensionsMainMenu) addExtensionsMainMenu; - decltype(EnumProjects) enumProjects; - decltype(GetSetProjectInfo_String) getSetProjectInfo_String; - decltype(GetProjectName) getProjectName; - decltype(ResolveRenderPattern) resolveRenderPattern; - decltype(EnumProjectMarkers2) enumProjectMarkers2; - decltype(EnumRegionRenderMatrix) enumRegionRenderMatrix; - decltype(GetParentTrack) getParentTrack; - decltype(GetTrackName) getTrackName; - decltype(Main_OnCommand) main_OnCommand; - decltype(GetProjectPathEx) getProjectPathEx; - decltype(ShowMessageBox) showMessageBox; - decltype(GetProjExtState) getProjExtState; - decltype(SetProjExtState) setProjExtState; - decltype(MarkProjectDirty) markProjectDirty; - int registerFunction(const char* command, void* function); - private: + struct ProjectInfo + { + ReaProject* projectReference{}; + juce::String projectName; + juce::String projectPath; + }; + + struct StateInfo + { + int projectStateCount{0}; + double renderSource{0.0}; + double renderBounds{0.0}; + juce::String renderFile; + juce::String renderPattern; + }; + std::vector<juce::String> splitDoubleNullTerminatedString(const char*); std::vector<juce::String> getItemListFromRenderPattern(ReaProject* project, const juce::String& pattern, bool suppressIllegalPaths = true); - juce::String getProjectString(ReaProject* project, const char* key, int bufferSize = 0); - ProjectInfo getProjectInfo(); + ProjectInfo getProjectInfo() const; + juce::String getRenderPattern(const ProjectInfo& projectInfo) const; + juce::File getRenderDirectory(const ProjectInfo& projectInfo) const; juce::File defaultRenderDirectory; - juce::String defaultRenderPattern; - reaper_plugin_info_t* pluginInfo; + juce::CriticalSection apiAccess; + + ReaperPluginInterface& reaperPluginInterface; + + StateInfo stateInfo; }; } // namespace AK::ReaWwise diff --git a/src/shared/Core/AssertHook.cpp b/src/shared/Core/AssertHook.cpp @@ -1,4 +1,4 @@ -#include "AK/Tools/Common/AkAssert.h" +#include <AK/Tools/Common/AkAssert.h> #include <cassert> #include <stdio.h> diff --git a/src/shared/Core/DawContext.h b/src/shared/Core/DawContext.h @@ -12,6 +12,7 @@ namespace AK::WwiseTransfer public: virtual ~DawContext() = default; + virtual bool sessionChanged() = 0; virtual juce::String getSessionName() = 0; virtual bool saveState(juce::ValueTree applicationState) = 0; virtual juce::ValueTree retrieveState() = 0; diff --git a/src/shared/Core/DawWatcher.cpp b/src/shared/Core/DawWatcher.cpp @@ -21,23 +21,22 @@ namespace AK::WwiseTransfer }; DawWatcher::DawWatcher(juce::ValueTree appState, WaapiClient& waapiClient, DawContext& dawContext, int refreshInterval) - : juce::Thread("DawWatcher") - , applicationState(appState) + : applicationState(appState) , hierarchyMapping(appState.getChildWithName(IDs::hierarchyMapping)) + , previewItems(appState.getChildWithName(IDs::previewItems)) , importDestination(appState, IDs::importDestination, nullptr) , originalsSubfolder(appState, IDs::originalsSubfolder, nullptr) , containerNameExists(appState, IDs::containerNameExists, nullptr) - , wwiseObjectsChanged(appState, IDs::wwiseObjectsChanged, nullptr) , previewLoading(appState, IDs::previewLoading, nullptr) - , dawContext(dawContext) - , waapiClient(waapiClient) - , previewItems(appState.getChildWithName(IDs::previewItems)) - , refreshInterval(refreshInterval) - , lastImportItemsFromDawHash(0) , sessionName(appState, IDs::sessionName, nullptr) , projectPath(appState, IDs::projectPath, nullptr) , originalsFolder(appState, IDs::originalsFolder, nullptr) , languageSubfolder(appState, IDs::languageSubfolder, nullptr) + , dawContext(dawContext) + , waapiClient(waapiClient) + , lastImportItemsHash(0) + , refreshInterval(refreshInterval) + , previewOptionsChanged(false) { auto featureSupport = appState.getChildWithName(IDs::featureSupport); waqlEnabled.referTo(featureSupport, IDs::waqlEnabled, nullptr); @@ -52,194 +51,194 @@ namespace AK::WwiseTransfer void DawWatcher::start() { - startThread(); + startTimer(refreshInterval); } void DawWatcher::stop() { - stopThread(-1); + stopTimer(); } - void DawWatcher::run() + void DawWatcher::timerCallback() { - while(!threadShouldExit()) + sessionName = dawContext.getSessionName(); + + if(dawContext.sessionChanged()) { - updateSessionName(); + triggerAsyncUpdate(); + } + } - std::optional<Import::Options> importOptions; - std::optional<PreviewOptions> previewOptions; + void DawWatcher::handleAsyncUpdate() + { + const auto hierarchyMappingPath = ImportHelper::hierarchyMappingToPath(ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping)); + const auto importItems = dawContext.getItemsForPreview({importDestination, originalsSubfolder, hierarchyMappingPath}); - { - juce::MessageManager::Lock l; + const auto importItemsHash = ImportHelper::importPreviewItemsToHash(importItems); - auto hierarchyMappingPath = ImportHelper::hierarchyMappingToPath(ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping)); + if(importItemsHash != lastImportItemsHash || previewOptionsChanged) + { + previewOptionsChanged = false; - importOptions = Import::Options(importDestination, originalsSubfolder, hierarchyMappingPath); - previewOptions = PreviewOptions{containerNameExists, projectPath, originalsFolder, waqlEnabled, languageSubfolder}; - } + std::set<juce::String> objectPaths; + std::unordered_map<juce::String, juce::ValueTree> pathToValueTreeMapping; - if(importOptions && previewOptions) - { - auto importItemsFromDaw = dawContext.getItemsForPreview(*importOptions); + juce::ValueTree rootNode(IDs::previewItems); - auto currentImportItemsFromDawHash = ImportHelper::importPreviewItemsToHash(importItemsFromDaw); + // Build tree based on import items and their ancestors + for(const auto& importItem : importItems) + { + auto currentNode = rootNode; - if(lastImportItemsFromDawHash != currentImportItemsFromDawHash || previewOptionsChanged) + for(const auto& ancestorPath : WwiseHelper::pathToAncestorPaths(importItem.path)) { - setPreviewLoading(true); - previewOptionsChanged.store(false); - - lastImportItemsFromDawHash = currentImportItemsFromDawHash; - - std::set<juce::String> objectPaths; - std::unordered_map<juce::String, juce::ValueTree> pathToValueTreeMapping; + auto pathWithoutType = WwiseHelper::pathToPathWithoutObjectTypes(ancestorPath); + auto child = currentNode.getChildWithName(pathWithoutType); - juce::ValueTree rootNode(IDs::previewItems); - - // Build tree based on import items and their ancestors - for(const auto& importItem : importItemsFromDaw) + if(!child.isValid()) { - auto currentNode = rootNode; - - for(const auto& ancestorPath : WwiseHelper::pathToAncestorPaths(importItem.path)) - { - auto pathWithoutType = WwiseHelper::pathToPathWithoutObjectTypes(ancestorPath); - auto child = currentNode.getChildWithName(pathWithoutType); - - if(!child.isValid()) - { - objectPaths.insert(pathWithoutType); - - auto name = WwiseHelper::pathToObjectName(pathWithoutType); - auto type = WwiseHelper::pathToObjectType(ancestorPath); + objectPaths.insert(pathWithoutType); - child = ImportHelper::previewItemNodeToValueTree(pathWithoutType, {name, type, Import::ObjectStatus::New, "", Import::WavStatus::Unknown}); + auto name = WwiseHelper::pathToObjectName(pathWithoutType); + auto type = WwiseHelper::pathToObjectType(ancestorPath); - currentNode.appendChild(child, nullptr); - pathToValueTreeMapping[pathWithoutType] = child; - } - - currentNode = child; - } + child = ImportHelper::previewItemNodeToValueTree(pathWithoutType, {name, type, Import::ObjectStatus::New, "", Import::WavStatus::Unknown}); - auto pathWithoutType = WwiseHelper::pathToPathWithoutObjectTypes(importItem.path); - objectPaths.insert(pathWithoutType); + currentNode.appendChild(child, nullptr); + pathToValueTreeMapping[pathWithoutType] = child; + } - auto name = WwiseHelper::pathToObjectName(importItem.path); - auto type = WwiseHelper::pathToObjectType(importItem.path); + currentNode = child; + } - auto originalsWav = previewOptions->languageSubfolder + juce::File::getSeparatorChar() + - (importItem.originalsSubFolder.isNotEmpty() ? importItem.originalsSubFolder + juce::File::getSeparatorChar() : "") + - juce::File(importItem.audioFilePath).getFileName(); + auto pathWithoutType = WwiseHelper::pathToPathWithoutObjectTypes(importItem.path); + objectPaths.insert(pathWithoutType); - auto wavStatus = Import::WavStatus::Unknown; + auto name = WwiseHelper::pathToObjectName(importItem.path); + auto type = WwiseHelper::pathToObjectType(importItem.path); - if(previewOptions->originalsFolder.isNotEmpty()) - { - auto absoluteWavPath = juce::File(previewOptions->originalsFolder).getChildFile(originalsWav); + auto originalsWav = languageSubfolder + juce::File::getSeparatorChar() + + (importItem.originalsSubFolder.isNotEmpty() ? importItem.originalsSubFolder + juce::File::getSeparatorChar() : "") + + juce::File(importItem.audioFilePath).getFileName(); - if(absoluteWavPath.exists()) - wavStatus = Import::WavStatus::Replaced; - else - wavStatus = Import::WavStatus::New; - } + auto wavStatus = Import::WavStatus::Unknown; - auto child = ImportHelper::previewItemNodeToValueTree(pathWithoutType, {name, type, Import::ObjectStatus::New, originalsWav, wavStatus}); + if(originalsFolder.get().isNotEmpty()) + { + auto absoluteWavPath = juce::File(originalsFolder).getChildFile(originalsWav); - currentNode.appendChild(child, nullptr); - pathToValueTreeMapping[pathWithoutType] = child; - } + if(absoluteWavPath.exists()) + wavStatus = Import::WavStatus::Replaced; + else + wavStatus = Import::WavStatus::New; + } - Waapi::Response<Waapi::ObjectResponseSet> response; + auto child = ImportHelper::previewItemNodeToValueTree(pathWithoutType, {name, type, Import::ObjectStatus::New, originalsWav, wavStatus}); - if(importOptions->importDestination.isNotEmpty()) - { - if(previewOptions->waqlEnabled) - response = waapiClient.getObjectAncestorsAndDescendants(importOptions->importDestination); - else - response = waapiClient.getObjectAncestorsAndDescendantsLegacy(importOptions->importDestination); - } + currentNode.appendChild(child, nullptr); + pathToValueTreeMapping[pathWithoutType] = child; + } - if(response.status) + auto onGetObjectAncestorsAndDescendants = [this, pathToValueTreeMapping, rootNode](const Waapi::Response<Waapi::ObjectResponseSet>& response) + { + if(response.status) + { + // Update original tree with information from existing objects + for(const auto& existingObject : response.result) { - auto pathToValueTreeMappingLocal = pathToValueTreeMapping; + auto it = pathToValueTreeMapping.find(existingObject.path); - // Update original tree with information from existing objects - for(const auto& existingObject : response.result) + if(it != pathToValueTreeMapping.end()) { - auto it = pathToValueTreeMappingLocal.find(existingObject.path); - - if(it != pathToValueTreeMappingLocal.end()) - { - auto previewItem = ImportHelper::valueTreeToPreviewItemNode(it->second); + auto previewItem = ImportHelper::valueTreeToPreviewItemNode(it->second); - if(existingObject.type != Wwise::ObjectType::Sound || previewOptions->containerNameExists == Import::ContainerNameExistsOption::UseExisting) - previewItem.objectStatus = Import::ObjectStatus::NoChange; - else if(previewOptions->containerNameExists == Import::ContainerNameExistsOption::Replace) - previewItem.objectStatus = Import::ObjectStatus::Replaced; - else if(previewOptions->containerNameExists == Import::ContainerNameExistsOption::CreateNew) - previewItem.objectStatus = Import::ObjectStatus::NewRenamed; + if(existingObject.type != Wwise::ObjectType::Sound || containerNameExists == Import::ContainerNameExistsOption::UseExisting) + previewItem.objectStatus = Import::ObjectStatus::NoChange; + else if(containerNameExists == Import::ContainerNameExistsOption::Replace) + previewItem.objectStatus = Import::ObjectStatus::Replaced; + else if(containerNameExists == Import::ContainerNameExistsOption::CreateNew) + previewItem.objectStatus = Import::ObjectStatus::NewRenamed; - if(previewItem.type == Wwise::ObjectType::Unknown) - { - previewItem.type = existingObject.type; - } - - it->second.copyPropertiesFrom(ImportHelper::previewItemNodeToValueTree(existingObject.path, previewItem), nullptr); + if(previewItem.type == Wwise::ObjectType::Unknown) + { + previewItem.type = existingObject.type; } + + juce::ValueTree localCopy = it->second; + localCopy.copyPropertiesFrom(ImportHelper::previewItemNodeToValueTree(existingObject.path, previewItem), nullptr); } } + } - // Update preview tree. This will cause the preview to update itself if there is new content - auto onCallAsync = [this, rootNode = rootNode] - { - if(!previewItems.isEquivalentTo(rootNode)) - previewItems.copyPropertiesAndChildrenFrom(rootNode, nullptr); - }; + if(!previewItems.isEquivalentTo(rootNode)) + previewItems.copyPropertiesAndChildrenFrom(rootNode, nullptr); - juce::MessageManager::callAsync(onCallAsync); + previewLoading = false; + }; - setPreviewLoading(false); - } - } + if(importDestination.get().isNotEmpty()) + { + previewLoading = true; - wait(refreshInterval); + if(waqlEnabled) + waapiClient.getObjectAncestorsAndDescendantsAsync(importDestination, onGetObjectAncestorsAndDescendants); + else + waapiClient.getObjectAncestorsAndDescendantsLegacyAsync(importDestination, onGetObjectAncestorsAndDescendants); + } + else + { + Waapi::Response<Waapi::ObjectResponseSet> emptyResponse; + onGetObjectAncestorsAndDescendants(emptyResponse); + } } } - void DawWatcher::setPreviewLoading(bool isPreviewLoading) + void DawWatcher::valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) { - auto onCallAsync = [this, isPreviewLoading] + static std::initializer_list<juce::Identifier> properties{IDs::containerNameExists, IDs::projectPath, IDs::originalsFolder, + IDs::wwiseObjectsChanged, IDs::waqlEnabled, IDs::languageSubfolder, IDs::originalsFolder, IDs::importDestination, IDs::originalsSubfolder}; + + if(treeWhosePropertyHasChanged == applicationState && std::find(properties.begin(), properties.end(), property) != properties.end() || + treeWhosePropertyHasChanged.getType() == IDs::hierarchyMappingNode) { - previewLoading = isPreviewLoading; - }; + // Used to notify the next update iteration that the preview may contain items that have changed that would not be reflected + // in the importItems hash. Perhaps we can include these items in the hash in the future. + previewOptionsChanged = true; - juce::MessageManager::callAsync(onCallAsync); + if(property == IDs::wwiseObjectsChanged) + applicationState.setPropertyExcludingListener(this, IDs::wwiseObjectsChanged, false, nullptr); + + triggerAsyncUpdate(); + } } - void DawWatcher::updateSessionName() + void DawWatcher::valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded) { - auto name = dawContext.getSessionName(); + juce::ignoreUnused(childWhichHasBeenAdded); - auto updateSessionName = [this, name = name] + if(parentTree.getType() == IDs::hierarchyMapping) { - sessionName = name; - }; - - juce::MessageManager::callAsync(updateSessionName); + triggerAsyncUpdate(); + } } - void DawWatcher::valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) + void DawWatcher::valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) { - static std::initializer_list<juce::Identifier> properties{IDs::containerNameExists, IDs::projectPath, IDs::originalsFolder, - IDs::wwiseObjectsChanged, IDs::waqlEnabled, IDs::languageSubfolder}; + juce::ignoreUnused(childWhichHasBeenRemoved, indexFromWhichChildWasRemoved); - if(treeWhosePropertyHasChanged == applicationState && std::find(properties.begin(), properties.end(), property) != properties.end()) + if(parentTree.getType() == IDs::hierarchyMapping) { - previewOptionsChanged.store(true); + triggerAsyncUpdate(); + } + } - if(property == IDs::wwiseObjectsChanged) - wwiseObjectsChanged = false; + void DawWatcher::valueTreeChildOrderChanged(juce::ValueTree& parentTreeWhoseChildrenHaveMoved, int oldIndex, int newIndex) + { + juce::ignoreUnused(oldIndex, newIndex); + + if(parentTreeWhoseChildrenHaveMoved.getType() == IDs::hierarchyMapping) + { + triggerAsyncUpdate(); } } } // namespace AK::WwiseTransfer diff --git a/src/shared/Core/DawWatcher.h b/src/shared/Core/DawWatcher.h @@ -8,7 +8,8 @@ namespace AK::WwiseTransfer { class DawWatcher - : private juce::Thread + : private juce::Timer + , public juce::AsyncUpdater , private juce::ValueTree::Listener { public: @@ -18,11 +19,14 @@ namespace AK::WwiseTransfer void start(); void stop(); + private: + juce::ValueTree applicationState; juce::ValueTree hierarchyMapping; + juce::ValueTree previewItems; + juce::CachedValue<juce::String> importDestination; juce::CachedValue<juce::String> originalsSubfolder; juce::CachedValue<Import::ContainerNameExistsOption> containerNameExists; - juce::CachedValue<bool> wwiseObjectsChanged; juce::CachedValue<bool> previewLoading; juce::CachedValue<juce::String> sessionName; juce::CachedValue<bool> waqlEnabled; @@ -30,24 +34,18 @@ namespace AK::WwiseTransfer juce::CachedValue<juce::String> originalsFolder; juce::CachedValue<juce::String> languageSubfolder; - juce::ValueTree previewItems; - - unsigned int lastImportItemsFromDawHash; - Import::ContainerNameExistsOption lastContainerNameExists; - juce::String lastProjectPath; - - private: - juce::ValueTree applicationState; - - std::atomic<bool> previewOptionsChanged; - DawContext& dawContext; WaapiClient& waapiClient; + + unsigned int lastImportItemsHash; int refreshInterval; + bool previewOptionsChanged; - void run() override; - void setPreviewLoading(bool isPreviewLoading); - void updateSessionName(); + void timerCallback() override; void valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) override; + void valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded) override; + void valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) override; + void valueTreeChildOrderChanged(juce::ValueTree& parentTreeWhoseChildrenHaveMoved, int oldIndex, int newIndex) override; + void handleAsyncUpdate() override; }; } // namespace AK::WwiseTransfer diff --git a/src/shared/Core/ImportTask.h b/src/shared/Core/ImportTask.h @@ -50,10 +50,8 @@ namespace AK::WwiseTransfer auto pathWithoutObjectTypes = WwiseHelper::pathToPathWithoutObjectTypes(importItem.path); objectsInExtension.insert(pathWithoutObjectTypes); - for(auto ancestorPath : WwiseHelper::pathToAncestorPaths(pathWithoutObjectTypes)) - { + for(const auto& ancestorPath : WwiseHelper::pathToAncestorPaths(pathWithoutObjectTypes)) objectsInExtension.insert(ancestorPath); - } } // Will be eventullay compared to the results of the import to figure out what was newly created @@ -77,37 +75,84 @@ namespace AK::WwiseTransfer // Ignore sounds that would be newly created. We need to get their paths from the import response because they may change. if(options.containerNameExistsOption != Import::ContainerNameExistsOption::CreateNew || object.type != Wwise::ObjectType::Sound) { - summary.objects[object.path].type = object.type; - summary.objects[object.path].newlyCreated = false; + auto& summaryObject = summary.objects[object.path]; + summaryObject.type = object.type; + summaryObject.id = object.id; } } - }; + } - if(options.undoGroupFeatureEnabled) - waapiClient.beginUndoGroup(); + const ScopedUndoGroup scopedundogroup(waapiClient, options.undoGroupFeatureEnabled); juce::String objectLanguage = ImportTaskContants::defaultObjectLanguage; if(options.hierarchyMappingNodeList.back().type == Wwise::ObjectType::SoundVoice) objectLanguage = options.hierarchyMappingNodeList.back().language; + // Check if wav files will be replaced. Only works if we have the originals folder. + // Basically checks to see if the audio file is already present in the originals folder. + std::set<juce::String> existingAudioFiles; + + if(options.originalsFolder.isNotEmpty()) + { + for(const auto& importItemRequest : importItemRequests) + { + // Build the final file path + auto pathInWwise = options.originalsFolder + options.languageSubfolder + juce::File::getSeparatorString() + + importItemRequest.originalsSubFolder + juce::File::getSeparatorString() + + juce::File(importItemRequest.renderFilePath).getFileName(); + + if(juce::File(pathInWwise).exists()) + existingAudioFiles.emplace(pathInWwise); + + for(const auto& existingObject : existingObjectsResponse.result) + { + if(existingObject.originalWavFilePath == pathInWwise) + { + auto& summaryObject = summary.objects[existingObject.path]; + summaryObject.id = existingObject.id; + summaryObject.originalWavFilePath = pathInWwise; + summaryObject.wavStatus = Import::WavStatus::Replaced; + summaryObject.type = existingObject.type; + } + } + } + } + auto importResponse = waapiClient.import(importItemRequests, options.containerNameExistsOption, objectLanguage); if(importResponse.status) { - // Anything that is returned here was newly created + // Result will include newly created and existing (affected) objects for(const auto& object : importResponse.result) { - summary.objectsCreated++; - - summary.objects[object.path].type = object.type; - summary.objects[object.path].newlyCreated = true; + // Check against existing objects to see if object was truely newly created + auto it = summary.objects.find(object.path); - // Audio file sources represent imported wav files - if(object.type == Wwise::ObjectType::AudioFileSource) + if(it == summary.objects.end()) { - summary.audioFilesImported++; + auto& summaryObject = summary.objects[object.path]; + summaryObject.objectStatus = Import::ObjectStatus::New; + summaryObject.type = object.type; + summaryObject.originalWavFilePath = object.originalWavFilePath; - summary.objects[object.path].originalWavFilePath = object.originalWavFilePath; + if(object.originalWavFilePath.isNotEmpty()) + { + // Some objects are associated with an originalWavFilePath that may have been replaced. + auto it = existingAudioFiles.find(object.originalWavFilePath); + if(it != existingAudioFiles.end()) + { + summary.objects[object.path].wavStatus = Import::WavStatus::Replaced; + } + else + { + summary.objects[object.path].wavStatus = Import::WavStatus::New; + } + } + } + // If the object was found but the id is different, it was replaced + else if(it->second.id != object.id) + { + summary.objects[object.path].objectStatus = Import::ObjectStatus::Replaced; } } @@ -143,16 +188,13 @@ namespace AK::WwiseTransfer auto it = depthToTemplatePropertyPathMap.find(depth); - if(it != depthToTemplatePropertyPathMap.end() && (options.applyTemplateOption == Import::ApplyTemplateOption::Always || object.newlyCreated)) - { + if(it != depthToTemplatePropertyPathMap.end() && (options.applyTemplateOption == Import::ApplyTemplateOption::Always || object.objectStatus == Import::ObjectStatus::New)) propertyTemplatePathToObjectMapping[it->second].emplace_back(objectPath); - } } for(const auto& [source, targets] : propertyTemplatePathToObjectMapping) { - auto response = waapiClient.pasteProperties({source, targets}); - + const auto response = waapiClient.pasteProperties({source, targets}); if(!response.status) { summary.errorMessage << juce::NewLine() << response.errorMessage; @@ -160,11 +202,7 @@ namespace AK::WwiseTransfer else { for(const auto& target : targets) - { - summary.objectTemplatesApplied++; - summary.objects[target].propertyTemplatePath = source; - } } } } @@ -193,9 +231,6 @@ namespace AK::WwiseTransfer summary.errorMessage << juce::NewLine() << existingObjectsResponse.errorMessage; } - if(options.undoGroupFeatureEnabled) - waapiClient.endUndoGroup("Import and Apply Paste Properties"); - auto onCallAsync = [this, summary = summary] { callback(summary); @@ -205,6 +240,26 @@ namespace AK::WwiseTransfer } private: + struct ScopedUndoGroup final + { + WaapiClient& waapiClient; + bool enabled{false}; + + ScopedUndoGroup(WaapiClient& waapiClient, bool enable) + : waapiClient(waapiClient) + , enabled(enable) + { + if(enable) + waapiClient.beginUndoGroup(); + } + + ~ScopedUndoGroup() + { + if(enabled) + waapiClient.endUndoGroup("Import and Apply Paste Properties"); + } + }; + WaapiClient& waapiClient; Import::Task::Options options; Callback callback; diff --git a/src/shared/Core/Logger.cpp b/src/shared/Core/Logger.cpp @@ -32,7 +32,7 @@ namespace AK::WwiseTransfer void Logger::addListener(IListener& listener) { listeners.add(&listener); - }; + } void Logger::removeListener(IListener& listener) { diff --git a/src/shared/Core/WaapiClient.cpp b/src/shared/Core/WaapiClient.cpp @@ -4,9 +4,7 @@ #include "Model/IDs.h" #include <JSONHelpers.h> -#include <algorithm> #include <juce_events/juce_events.h> -#include <memory> #include <set> namespace AK::WwiseTransfer @@ -436,6 +434,7 @@ namespace AK::WwiseTransfer AkVariant{"name"}, AkVariant{"type"}, AkVariant{"path"}, + AkVariant{"sound:originalWavFilePath"}, AkVariant{"workunitType"}, }, }, @@ -609,6 +608,7 @@ namespace AK::WwiseTransfer AkVariant{"name"}, AkVariant{"type"}, AkVariant{"path"}, + AkVariant{"sound:originalWavFilePath"}, AkVariant{"workunitType"}, }, }, diff --git a/src/shared/Core/WaapiClient.h b/src/shared/Core/WaapiClient.h @@ -2,17 +2,13 @@ #include "AK/WwiseAuthoringAPI/AkAutobahn/AkJson.h" #include "AK/WwiseAuthoringAPI/AkAutobahn/Client.h" -#include "AK/WwiseAuthoringAPI/waapi.h" #include "Helpers/WaapiHelper.h" #include "Model/Import.h" #include "Model/Waapi.h" #include "Model/Wwise.h" #include <juce_core/juce_core.h> -#include <juce_data_structures/juce_data_structures.h> #include <juce_events/juce_events.h> -#include <optional> -#include <set> namespace AK::WwiseTransfer { @@ -171,6 +167,28 @@ namespace AK::WwiseTransfer threadPool.addJob(new AsyncJob(onJobExecute, callback), true); } + template <typename Callback> + void getObjectAncestorsAndDescendantsAsync(const juce::String& objectPath, Callback& callback) + { + auto onJobExecute = [objectPath, this]() + { + return getObjectAncestorsAndDescendants(objectPath); + }; + + threadPool.addJob(new AsyncJob(onJobExecute, callback), true); + } + + template <typename Callback> + void getObjectAncestorsAndDescendantsLegacyAsync(const juce::String& objectPath, Callback& callback) + { + auto onJobExecute = [objectPath, this]() + { + return getObjectAncestorsAndDescendantsLegacy(objectPath); + }; + + threadPool.addJob(new AsyncJob(onJobExecute, callback), true); + } + private: juce::ThreadPool threadPool; }; diff --git a/src/shared/Model/Import.h b/src/shared/Model/Import.h @@ -3,6 +3,8 @@ #include "Model/IDs.h" #include "Model/Wwise.h" +#include <algorithm> + namespace AK::WwiseTransfer::Import { enum class ContainerNameExistsOption : int @@ -90,14 +92,14 @@ namespace AK::WwiseTransfer::Import } juce::String name; - bool nameValid; + bool nameValid{}; juce::String nameErrorMessage; - Wwise::ObjectType type; - bool typeValid; + Wwise::ObjectType type{}; + bool typeValid{}; juce::String typeErrorMessage; juce::String propertyTemplatePath; - bool propertyTemplatePathEnabled; - bool propertyTemplatePathValid; + bool propertyTemplatePathEnabled{}; + bool propertyTemplatePathValid{}; juce::String propertyTemplatePathErrorMessage; juce::String language; }; @@ -120,19 +122,36 @@ namespace AK::WwiseTransfer::Import { struct Object { - Wwise::ObjectType type; - juce::String originalWavFilePath; + juce::String id; + Wwise::ObjectType type{}; juce::String propertyTemplatePath; - bool newlyCreated{false}; + juce::String originalWavFilePath; + Import::WavStatus wavStatus{}; + Import::ObjectStatus objectStatus{}; }; std::map<juce::String, Object> objects; + juce::String errorMessage; - int objectsCreated{0}; - int audioFilesImported{0}; - int objectTemplatesApplied{0}; + using PathObjectPair = std::pair<juce::String, Object>; - juce::String errorMessage; + int getNumObjectsCreated() const + { + auto predicate = [](const PathObjectPair& pathObjectPair) + { + return pathObjectPair.second.objectStatus == Import::ObjectStatus::New; + }; + return std::count_if(objects.begin(), objects.end(), predicate); + } + + int getNumObjectTemplatesApplied() const + { + auto predicate = [](const PathObjectPair& pathObjectPair) + { + return pathObjectPair.second.propertyTemplatePath.isNotEmpty(); + }; + return std::count_if(objects.begin(), objects.end(), predicate); + } }; namespace Task @@ -140,15 +159,16 @@ namespace AK::WwiseTransfer::Import struct Options { std::vector<Import::Item> importItems; - Import::ContainerNameExistsOption containerNameExistsOption; - Import::ApplyTemplateOption applyTemplateOption; + Import::ContainerNameExistsOption containerNameExistsOption{}; + Import::ApplyTemplateOption applyTemplateOption{Import::ApplyTemplateOption::Always}; juce::String importDestination; std::vector<Import::HierarchyMappingNode> hierarchyMappingNodeList; + juce::String originalsFolder; + juce::String languageSubfolder; juce::String selectObjectsOnImportCommand; bool applyTemplateFeatureEnabled{false}; bool undoGroupFeatureEnabled{false}; bool waqlEnabled{false}; - juce::String objectLanguage; }; } // namespace Task } // namespace AK::WwiseTransfer::Import diff --git a/src/shared/Persistance/ApplicationProperties.cpp b/src/shared/Persistance/ApplicationProperties.cpp @@ -73,21 +73,30 @@ namespace AK::WwiseTransfer return recentHierarchyMappingPresets; } + void ApplicationProperties::addRecentHierarchyMappingPreset(const juce::String& path) { using namespace ApplicationPropertyConstants; auto recentHierarchyMappingPresets = getRecentHierarchyMappingPresets(); - if(recentHierarchyMappingPresets.contains(path)) - { recentHierarchyMappingPresets.removeString(path); - } recentHierarchyMappingPresets.insert(0, path); - auto recentHierarchyMappingPresetsAsString = recentHierarchyMappingPresets.joinIntoString(";", 0, 10); + const auto recentHierarchyMappingPresetsAsString = recentHierarchyMappingPresets.joinIntoString(";", 0, 10); + getUserSettings()->setValue(recentHierarchyMappingPresetsPropertyName, recentHierarchyMappingPresetsAsString); + } + + void ApplicationProperties::removeRecentHierarchyMappingPreset(const juce::String& path) + { + using namespace ApplicationPropertyConstants; + + auto recentHierarchyMappingPresets = getRecentHierarchyMappingPresets(); + if (recentHierarchyMappingPresets.contains(path)) + recentHierarchyMappingPresets.removeString(path); + const auto recentHierarchyMappingPresetsAsString = recentHierarchyMappingPresets.joinIntoString(";", 0, 10); getUserSettings()->setValue(recentHierarchyMappingPresetsPropertyName, recentHierarchyMappingPresetsAsString); } diff --git a/src/shared/Persistance/ApplicationProperties.h b/src/shared/Persistance/ApplicationProperties.h @@ -14,6 +14,7 @@ namespace AK::WwiseTransfer int getPreviewRefreshInterval(); juce::StringArray getRecentHierarchyMappingPresets(); void addRecentHierarchyMappingPreset(const juce::String& path); + void removeRecentHierarchyMappingPreset(const juce::String& path); void clearRecentHierarchyMappingPresets(); double getScaleFactorOverride(); bool getShowSilentIncrementWarning(); diff --git a/src/shared/Persistance/ApplicationStateValidator.cpp b/src/shared/Persistance/ApplicationStateValidator.cpp @@ -164,7 +164,7 @@ namespace AK::WwiseTransfer::ApplicationState // TODO: Should error be reported on parent or child? auto hierarchyMappingNodeList = ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping); - for(int i = 0; i < hierarchyMappingNodeList.size(); ++i) + for(std::size_t i = 0; i < hierarchyMappingNodeList.size(); ++i) { auto& child = hierarchyMappingNodeList.at(i); diff --git a/src/shared/UI/ImportControlsComponent.cpp b/src/shared/UI/ImportControlsComponent.cpp @@ -3,6 +3,8 @@ #include "Helpers/ImportHelper.h" #include "Model/IDs.h" +#include <set> + namespace AK::WwiseTransfer { enum MessageBoxOption @@ -24,6 +26,8 @@ namespace AK::WwiseTransfer , importDestinationValid(applicationState, IDs::importDestinationValid, nullptr) , importDestination(applicationState, IDs::importDestination, nullptr) , originalsSubFolder(applicationState, IDs::originalsSubfolder, nullptr) + , originalsFolder(applicationState, IDs::originalsFolder, nullptr) + , languageSubfolder(applicationState, IDs::languageSubfolder, nullptr) , projectPath(applicationState, IDs::projectPath, nullptr) , containerNameExistsOption(applicationState, IDs::containerNameExists, nullptr) , applyTemplateOption(applicationState, IDs::applyTemplate, nullptr) @@ -39,6 +43,7 @@ namespace AK::WwiseTransfer selectObjectsOnImportCommand.referTo(featureSupport, IDs::selectObjectsOnImportCommand, nullptr); applyTemplateFeatureEnabled.referTo(featureSupport, IDs::applyTemplateFeatureEnabled, nullptr); undoGroupFeatureEnabled.referTo(featureSupport, IDs::undoGroupFeatureEnabled, nullptr); + waqlEnabled.referTo(featureSupport, IDs::waqlEnabled, nullptr); importButton.setButtonText("Transfer to Wwise"); @@ -67,18 +72,54 @@ namespace AK::WwiseTransfer importButton.setBounds(getLocalBounds()); } + namespace + { + bool RenderDirContentModified(const std::set<juce::File>& directorySet, const juce::Time& lastWriteTime) + { + for(const auto& directory : directorySet) + { + for(const auto& file : directory.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false)) + { + if(file.getLastModificationTime() > lastWriteTime) + { + return true; + } + } + } + return false; + } + } // namespace + void ImportControlsComponent::onImportButtonClick() { using namespace ImportControlsComponentConstants; - // Disbale the import button while rendering + // Disable the import button while rendering importButton.setEnabled(false); + const auto hierarchyMappingPath = ImportHelper::hierarchyMappingToPath(ImportHelper::valueTreeToHierarchyMappingNodeList(applicationState.getChildWithName(IDs::hierarchyMapping))); + const Import::Options opts(importDestination, originalsSubFolder, hierarchyMappingPath); + + const auto previewItems = dawContext.getItemsForPreview(opts); + std::set<juce::File> directorySet; + for(const auto item : previewItems) + { + directorySet.insert(juce::File(item.audioFilePath).getParentDirectory()); + } + auto lastModificationTime = juce::Time::getCurrentTime(); + juce::Logger::writeToLog("Sending render request to DAW"); dawContext.renderItems(); - const auto hierarchyMappingPath = ImportHelper::hierarchyMappingToPath(ImportHelper::valueTreeToHierarchyMappingNodeList(applicationState.getChildWithName(IDs::hierarchyMapping))); - const Import::Options opts(importDestination, originalsSubFolder, hierarchyMappingPath); + if(!RenderDirContentModified(directorySet, lastModificationTime)) + { + const juce::String message("One or more files failed to render."); + juce::Logger::writeToLog(message); + juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Import Aborted", message); + importButton.setEnabled(true); + return; + } + const auto importItems = dawContext.getItemsForImport(opts); bool showRenameWarning = false; @@ -92,7 +133,8 @@ namespace AK::WwiseTransfer showRenderFailed = true; break; } - else if(importItem.audioFilePath != importItem.renderFilePath) + + if(importItem.audioFilePath != importItem.renderFilePath) { showRenameWarning = true; break; @@ -122,9 +164,12 @@ namespace AK::WwiseTransfer applyTemplateOption, importDestination, hierarchyMappingNodeList, + originalsFolder, + languageSubfolder, selectObjectsOnImportCommand, applyTemplateFeatureEnabled, - undoGroupFeatureEnabled}; + undoGroupFeatureEnabled, + waqlEnabled}; auto onImportComplete = [this, importTaskOptions = importTaskOptions](const Import::Summary& importSummary) { @@ -161,10 +206,10 @@ namespace AK::WwiseTransfer }; auto messageBoxOptions = juce::MessageBoxOptions() - .withTitle("Action Required") - .withMessage(message) - .withButton("Continue") - .withButton("Cancel"); + .withTitle("Action Required") + .withMessage(message) + .withButton("Continue") + .withButton("Cancel"); juce::AlertWindow::showAsync(messageBoxOptions, onDialogBtnClicked); @@ -183,9 +228,9 @@ namespace AK::WwiseTransfer { juce::String message; - message << summary.objectsCreated << " object(s) created."; - message << juce::NewLine() << summary.objectTemplatesApplied << " object template(s) applied."; - message << juce::NewLine() << summary.audioFilesImported << " audio files(s) imported."; + message << summary.getNumObjectsCreated() << " object(s) created."; + message << juce::NewLine() << summary.getNumObjectTemplatesApplied() << " object template(s) applied."; + message << juce::NewLine() << importTaskOptions.importItems.size() << " audio files(s) imported."; if(summary.errorMessage.isNotEmpty()) message << juce::NewLine() << summary.errorMessage; @@ -225,15 +270,17 @@ namespace AK::WwiseTransfer importSummaryFile.appendText("Container Name Exists: " + ImportHelper::containerNameExistsOptionToReadableString(importTaskOptions.containerNameExistsOption) + "\n"); importSummaryFile.appendText("Apply Template: " + ImportHelper::applyTemplateOptionToReadableString(importTaskOptions.applyTemplateOption) + "\n\n"); - importSummaryFile.appendText("Objects created: " + juce::String(summary.objectsCreated) + "\n"); - importSummaryFile.appendText("Object Templates Applied: " + juce::String(summary.objectTemplatesApplied) + "\n"); - importSummaryFile.appendText("Audio Files Imported: " + juce::String(summary.audioFilesImported) + "\n\n"); + importSummaryFile.appendText("Objects created: " + juce::String(summary.getNumObjectsCreated()) + "\n"); + importSummaryFile.appendText("Object Templates Applied: " + juce::String(summary.getNumObjectTemplatesApplied()) + "\n"); + importSummaryFile.appendText("Audio Files Imported: " + juce::String(importTaskOptions.importItems.size()) + "\n\n"); - importSummaryFile.appendText("<table><tr><th>Object Path</th><th>Type</th><th>Created</th><th>Originals Wav</th><th>Property Template Applied</th></tr>"); + importSummaryFile.appendText("<table><tr><th>Object Path</th><th>Type</th><th>Object Status</th><th>Originals Wav</th><th>Wav Status</th><th>Property Template Applied</th></tr>"); for(const auto& [objectPath, object] : summary.objects) { - importSummaryFile.appendText("<tr><td>" + objectPath + "</td><td>" + WwiseHelper::objectTypeToReadableString(object.type) + "</td><td>" + (object.newlyCreated ? "X" : "") + "</td><td>" + object.originalWavFilePath + "</td><td>" + object.propertyTemplatePath + "</td></tr>"); + importSummaryFile.appendText("<tr><td>" + objectPath + "</td><td>" + WwiseHelper::objectTypeToReadableString(object.type) + "</td>" + + "<td>" + ImportHelper::objectStatusToReadableString(object.objectStatus) + "</td><td>" + object.originalWavFilePath + "</td>" + + "<td>" + ImportHelper::wavStatusToReadableString(object.wavStatus) + "</td><td>" + object.propertyTemplatePath + "</td></tr>"); } importSummaryFile.appendText("</table></pre>"); diff --git a/src/shared/UI/ImportControlsComponent.h b/src/shared/UI/ImportControlsComponent.h @@ -27,7 +27,9 @@ namespace AK::WwiseTransfer juce::CachedValue<bool> importDestinationValid; juce::CachedValue<juce::String> importDestination; juce::CachedValue<juce::String> originalsSubFolder; + juce::CachedValue<juce::String> originalsFolder; juce::CachedValue<juce::String> projectPath; + juce::CachedValue<juce::String> languageSubfolder; juce::CachedValue<Import::ContainerNameExistsOption> containerNameExistsOption; juce::CachedValue<Import::ApplyTemplateOption> applyTemplateOption; juce::ValueTree hierarchyMapping; @@ -35,6 +37,7 @@ namespace AK::WwiseTransfer juce::CachedValue<juce::String> selectObjectsOnImportCommand; juce::CachedValue<bool> applyTemplateFeatureEnabled; juce::CachedValue<bool> undoGroupFeatureEnabled; + juce::CachedValue<bool> waqlEnabled; WaapiClient& waapiClient; DawContext& dawContext; diff --git a/src/shared/UI/PresetMenuComponent.cpp b/src/shared/UI/PresetMenuComponent.cpp @@ -6,6 +6,15 @@ namespace AK::WwiseTransfer { + namespace PresetMenuComponentConstants + { + constexpr auto loadFlags = juce::FileBrowserComponent::openMode | + juce::FileBrowserComponent::canSelectFiles; + constexpr auto saveFlags = juce::FileBrowserComponent::saveMode | + juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::warnAboutOverwriting; + constexpr auto fileFilter = "*.xml"; + } // namespace PresetMenuComponentConstants + PresetMenuComponent::PresetMenuComponent(juce::ValueTree appState, ApplicationProperties& applicationProperties, const juce::String& applicationName) : CustomDrawableButton("PresetMenuButton", juce::Drawable::createFromImageData(BinaryData::General_FolderWithTriangle_Normal_svg, BinaryData::General_FolderWithTriangle_Normal_svgSize)) , hierarchyMapping(appState.getChildWithName(IDs::hierarchyMapping)) @@ -23,22 +32,42 @@ namespace AK::WwiseTransfer void PresetMenuComponent::showMenu() { - auto opts = juce::PopupMenu::Options() - .withTargetComponent(this); + using namespace PresetMenuComponentConstants; juce::PopupMenu presetMenu; presetMenu.setLookAndFeel(&getLookAndFeel()); auto onSavePreset = [this] { - savePreset(); + fileChooser = std::make_unique<juce::FileChooser>("Save preset...", presetFolder, fileFilter); + + auto onFileChosen = [this](const juce::FileChooser& chooser) + { + auto file = chooser.getResult(); + file.create(); + + const auto presetData = PersistanceHelper::hierarchyMappingToPresetData(hierarchyMapping); + file.replaceWithText(presetData); + applicationProperties.addRecentHierarchyMappingPreset(file.getFullPathName()); + }; + + fileChooser->launchAsync(saveFlags, onFileChosen); }; presetMenu.addItem("Save Preset...", onSavePreset); auto onLoadPreset = [this] { - loadPresetWithFilePicker(); + fileChooser = std::make_unique<juce::FileChooser>("Select preset...", presetFolder, fileFilter); + + auto onFileChosen = [this](const juce::FileChooser& chooser) + { + auto presetFile = chooser.getResult(); + if (presetFile.exists()) + loadPreset(presetFile); + }; + + fileChooser->launchAsync(loadFlags, onFileChosen); }; presetMenu.addItem("Load Preset...", onLoadPreset); @@ -68,24 +97,10 @@ namespace AK::WwiseTransfer presetMenu.addSubMenu("Recent Preset", recentMenu, !recentPresets.isEmpty()); + const auto opts = juce::PopupMenu::Options().withTargetComponent(this); presetMenu.showMenuAsync(opts); } - void PresetMenuComponent::loadPresetWithFilePicker() - { - fileChooser = std::make_unique<juce::FileChooser>("Select preset...", presetFolder, "*.xml"); - - auto onFileChosen = [this](const juce::FileChooser& chooser) - { - auto presetFile = chooser.getResult(); - - if(presetFile.exists()) - loadPreset(presetFile); - }; - - fileChooser->launchAsync(juce::FileBrowserComponent::openMode, onFileChosen); - } - void PresetMenuComponent::loadPreset(const juce::File& presetFile) { auto presetData = presetFile.loadFileAsString(); @@ -99,26 +114,8 @@ namespace AK::WwiseTransfer else { juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Load Preset", "Unable to load preset! The preset file is corrupt."); + applicationProperties.removeRecentHierarchyMappingPreset(presetFile.getFullPathName()); } } - void PresetMenuComponent::savePreset() - { - fileChooser = std::make_unique<juce::FileChooser>("Save preset...", presetFolder, "*.xml"); - - auto onFileChosen = [this](const juce::FileChooser& chooser) - { - auto presetData = PersistanceHelper::hierarchyMappingToPresetData(hierarchyMapping); - - auto file = chooser.getResult(); - - file.create(); - - file.replaceWithText(presetData); - - applicationProperties.addRecentHierarchyMappingPreset(file.getFullPathName()); - }; - - fileChooser->launchAsync(juce::FileBrowserComponent::saveMode, onFileChosen); - } } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/PresetMenuComponent.h b/src/shared/UI/PresetMenuComponent.h @@ -22,9 +22,7 @@ namespace AK::WwiseTransfer ApplicationProperties& applicationProperties; void showMenu(); - void loadPresetWithFilePicker(); void loadPreset(const juce::File& filename); - void savePreset(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PresetMenuComponent) }; diff --git a/src/shared/UI/SelectedRowPropertiesComponent.cpp b/src/shared/UI/SelectedRowPropertiesComponent.cpp @@ -20,7 +20,7 @@ namespace AK::WwiseTransfer constexpr int syncButtonWidth = 36; constexpr int propertyTemplateToggleButtonWidth = 22; constexpr int lineThickness = 2; - constexpr std::initializer_list<Wwise::ObjectType> objectTypes{Wwise::ObjectType::ActorMixer, Wwise::ObjectType::AudioFileSource, Wwise::ObjectType::BlendContainer, + constexpr std::initializer_list<Wwise::ObjectType> objectTypes{Wwise::ObjectType::BlendContainer, Wwise::ObjectType::PhysicalFolder, Wwise::ObjectType::RandomContainer, Wwise::ObjectType::SequenceContainer, Wwise::ObjectType::SoundSFX, Wwise::ObjectType::SoundVoice, Wwise::ObjectType::SwitchContainer, Wwise::ObjectType::VirtualFolder, Wwise::ObjectType::WorkUnit}; const juce::String pastePropertiesToolTip = "Paste properties are only available when connected to Wwise 2022+"; @@ -37,8 +37,6 @@ namespace AK::WwiseTransfer { using namespace SelectedRowPropertiesComponentConstants; - // setText("Selected Row Properties"); - setText("Selected Row Properties"); auto featureSupport = applicationState.getChildWithName(IDs::featureSupport); diff --git a/src/standalone/StubContext.h b/src/standalone/StubContext.h @@ -10,6 +10,11 @@ namespace AK::WwiseTransfer : public DawContext { public: + bool sessionChanged() override + { + return false; + } + std::vector<Import::PreviewItem> getItemsForPreview(const Import::Options& options) override { return std::vector<Import::PreviewItem>{{"\\A\\B\\C", juce::String(), juce::String()}};