diff --git a/Music Player.meta b/Music Player.meta new file mode 100644 index 0000000..7f1d97e --- /dev/null +++ b/Music Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be53b5399a162714babbb23c9db40b9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Editor.meta b/Music Player/Editor.meta new file mode 100644 index 0000000..b36d0c9 --- /dev/null +++ b/Music Player/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: faf352127d53f224da7f41d14735821a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Editor/MusicPlayer.cs b/Music Player/Editor/MusicPlayer.cs new file mode 100644 index 0000000..f7b08d4 --- /dev/null +++ b/Music Player/Editor/MusicPlayer.cs @@ -0,0 +1,172 @@ +#if UNITY_EDITOR +using UnityEngine; +using System.Collections.Generic; +using VRC.SDK3.Avatars.ScriptableObjects; +using nadena.dev.ndmf; +using gay.lilyy.MusicPlayer.Editor; +using AnimatorAsCode.V1; +using VRC.SDK3.Avatars.Components; +using AnimatorAsCode.V1.ModularAvatar; +using UnityEditor; +using AnimatorAsCode.V1.VRC; +using NUnit.Framework.Constraints; +using System.Linq; +using System.Threading.Tasks; + +[assembly: ExportsPlugin(typeof(MusicPlayerPlugin))] + +namespace gay.lilyy.MusicPlayer.Editor +{ + public class MusicPlayerPlugin : Plugin + { + public override string QualifiedName => "gay.lilyy.MusicPlayer"; + public override string DisplayName => "Music Player"; + + private const string SystemName = "MusicPlayer"; + private const bool UseWriteDefaults = true; + protected override void Configure() + { + InPhase(BuildPhase.Generating).Run($"Generate {DisplayName}", Generate); + } + private void Generate(BuildContext ctx) + { + // Find all components of type ExampleToggle in this avatar. + var songs = ctx.AvatarRootTransform.GetComponentsInChildren(); + if (songs.Length == 0) return; // If there are none in the avatar, skip this entirely. + + var songGo = songs[0].gameObject; + List usedIndexes = new List(); + + foreach (var song in songs) + { + if (song.gameObject != songGo) + { + throw new System.Exception("All MusicPlayerSong components must be on the same GameObject."); + } + if (usedIndexes.Contains(song.TrackNumber)) + { + throw new System.Exception($"${song.Name} is using a duplicate track number: ${song.TrackNumber}"); + } + } + + var audioSource = songGo.GetComponent(); + // INSERT_YOUR_CODE + // Ensure all track numbers from 1 to max are present + int minTrackNumber = songs.Min(song => song.TrackNumber); + if (minTrackNumber < 1) { + throw new System.Exception("Track numbers are 1 and up"); + } + int maxTrackNumber = songs.Max(song => song.TrackNumber); + HashSet trackNumbers = new HashSet(songs.Select(song => song.TrackNumber)); + for (int i = 1; i <= maxTrackNumber; i++) + { + if (!trackNumbers.Contains(i)) + { + throw new System.Exception($"Missing track number: {i}. All track numbers from 1 to {maxTrackNumber} must be filled."); + } + } + + + + // Initialize Animator As Code. + var modularAvatar = MaAc.Create(new GameObject(SystemName) + { + transform = { parent = ctx.AvatarRootTransform } + }); + + + var controller = CreateController(ctx, modularAvatar, songs, audioSource); + + + // Create a new object in the scene. We will add Modular Avatar components inside it. + + // By creating a Modular Avatar Merge Animator component, + // our animator controller will be added to the avatar's FX layer. + modularAvatar.NewMergeAnimator(controller.AnimatorController, VRCAvatarDescriptor.AnimLayerType.FX); + Debug.Log("Added music player"); + } + + private AacFlController CreateController(BuildContext ctx, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) + { + var aac = AacV1.Create(new AacConfiguration + { + SystemName = SystemName, + AnimatorRoot = ctx.AvatarRootTransform, + DefaultValueRoot = ctx.AvatarRootTransform, + AssetKey = GUID.Generate().ToString(), + AssetContainer = ctx.AssetContainer, + ContainerMode = AacConfiguration.Container.OnlyWhenPersistenceRequired, + // (For AAC 1.2.0 and above) The next line is recommended starting from NDMF 1.6.0. + // If you use a lower version of NDMF or if you don't use it, remove that line. + AssetContainerProvider = new NDMFContainerProvider(ctx), + // States will be created with Write Defaults set to ON or OFF based on whether UseWriteDefaults is true or false. + DefaultsProvider = new AacDefaultsProvider(UseWriteDefaults) + }); + // Create a new animator controller. + // This will be merged with the rest of the playable layer at the end of this function. + var controller = aac.NewAnimatorController(); + + CreatePlayerLayer(controller, modularAvatar, songs, audioSource); + CreateVolumeLayer(aac, controller, modularAvatar, audioSource); + + return controller; + } + + private void CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource) + { + var playerLayer = controller.NewLayer("Player"); + var songIndexParam = playerLayer.IntParameter("SongIndex"); + modularAvatar.NewParameter(songIndexParam).NotSaved(); + var isLocal = playerLayer.BoolParameter("IsLocal"); + + var nonLocalState = playerLayer.NewState("NonLocal"); + var idleState = playerLayer.NewState("Idle"); + + nonLocalState.TransitionsTo(idleState).When(isLocal.IsEqualTo(true)); + // will never appear ingame, useful for testing + idleState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false)); + + foreach (var song in songs) + { + var songState = playerLayer.NewState($"Song ${song.Name}"); + + songState.TransitionsTo(idleState).When(songIndexParam.IsNotEqualTo(song.TrackNumber)); + // again, will never happen ingame. still useful + songState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false)); + idleState.TransitionsTo(songState).When(songIndexParam.IsEqualTo(song.TrackNumber)); + songState.Audio(audioSource, (audio) => + { + + audio.SelectsClip(0, new List { song.song }.ToArray() ); + audio.StopsPlayingOnExit(); + audio.StartsPlayingOnEnter(); + }); + } + } + private void CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) { + var songVolumeLayer = controller.NewLayer("Volume"); + var volumeParam = songVolumeLayer.FloatParameter("volume"); + modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.01f); + + var clip = aac.NewClip().Animating((action) => { + var animate = action.Animates(audioSource, "m_Volume"); + animate.WithSecondsUnit((seconds) => { + seconds.Linear(0, 0); + seconds.Linear(1, 1); + }); + }); + var state = songVolumeLayer.NewState("Volume"); + state.WithAnimation(clip).WithMotionTime(volumeParam); + } + } + + internal class NDMFContainerProvider : IAacAssetContainerProvider + { + private readonly BuildContext _ctx; + public NDMFContainerProvider(BuildContext ctx) => _ctx = ctx; + public void SaveAsPersistenceRequired(UnityEngine.Object objectToAdd) => _ctx.AssetSaver.SaveAsset(objectToAdd); + public void SaveAsRegular(UnityEngine.Object objectToAdd) { } // Let NDMF crawl our assets when it finishes + public void ClearPreviousAssets() { } // ClearPreviousAssets is never used in non-destructive contexts + } +} +#endif diff --git a/Music Player/Editor/MusicPlayer.cs.meta b/Music Player/Editor/MusicPlayer.cs.meta new file mode 100644 index 0000000..d96cad9 --- /dev/null +++ b/Music Player/Editor/MusicPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44013705dced1564689b5665ebb419da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Editor/MusicPlayerEditor.asmdef b/Music Player/Editor/MusicPlayerEditor.asmdef new file mode 100644 index 0000000..49a09ce --- /dev/null +++ b/Music Player/Editor/MusicPlayerEditor.asmdef @@ -0,0 +1,25 @@ +{ + "name": "MusicPlayer", + "rootNamespace": "", + "references": [ + "VRC.SDK3A", + "VRC.SDK3A.Editor", + "MusicPlayerRuntime", + "AnimatorAsCode.V1", + "AnimatorAsCode.V1.VRChat", + "AnimatorAsCode.V1.ModularAvatar", + "nadena.dev.ndmf", + "nadena.dev.ndmf.vrchat" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Music Player/Editor/MusicPlayerEditor.asmdef.meta b/Music Player/Editor/MusicPlayerEditor.asmdef.meta new file mode 100644 index 0000000..277db5b --- /dev/null +++ b/Music Player/Editor/MusicPlayerEditor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 16c177b70057c1640bd2e9ae8f06516e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Editor/MusicPlayerInspector.cs b/Music Player/Editor/MusicPlayerInspector.cs new file mode 100644 index 0000000..d456591 --- /dev/null +++ b/Music Player/Editor/MusicPlayerInspector.cs @@ -0,0 +1,168 @@ +// using UnityEditor; +// using UnityEngine; +// using System.Collections.Generic; +// using System.Linq; +// using VRC.SDK3.Avatars.ScriptableObjects; +// using VRC.SDK3.Avatars.Components; + +// namespace gay.lilyy.MusicPlayer +// { +// [CustomEditor(typeof(ParameterPreset))] +// public class ParameterPresetInspector : UnityEditor.Editor +// { +// private ParameterPreset preset; +// private bool parametersLoaded = false; + +// private void OnEnable() +// { +// preset = (ParameterPreset)target; +// LoadMissingParameters(); +// } + +// private void LoadMissingParameters() +// { +// VRCAvatarDescriptor aviDesc = preset.gameObject.GetComponent(); +// if (aviDesc.expressionParameters == null || parametersLoaded) +// return; + +// // Initialize Parameters list if it's null +// if (preset.Parameters == null) +// preset.Parameters = new List(); + +// // Get parameters from the Group's ParametersFile +// var sourceParameters = aviDesc.expressionParameters.parameters; +// if (sourceParameters == null) +// return; + +// // Create a dictionary of source parameters for quick lookup +// var sourceParamDict = sourceParameters.ToDictionary(p => p.name, p => p); + +// // Create a set of source parameter names +// var sourceParameterNames = new HashSet(sourceParamDict.Keys); + +// // Remove parameters that exist in preset but not in source +// bool hasChanges = false; +// preset.Parameters.RemoveAll(presetParam => +// { +// if (!sourceParameterNames.Contains(presetParam.name)) +// { +// hasChanges = true; +// return true; // Remove this parameter +// } +// return false; // Keep this parameter +// }); + +// // Add missing parameters and update existing ones +// foreach (var sourceParam in sourceParameters) +// { +// var existingParam = preset.Parameters.FirstOrDefault(p => p.name == sourceParam.name); + +// if (existingParam == null) +// { +// // Add new parameter +// var clonedParam = new PresetParameter +// { +// name = sourceParam.name, +// valueType = sourceParam.valueType, +// setTo = sourceParam.defaultValue, +// shouldChange = false, +// }; + +// preset.Parameters.Add(clonedParam); +// hasChanges = true; +// } +// else +// { +// // Update existing parameter to match source type +// if (existingParam.valueType != sourceParam.valueType) +// { +// existingParam.valueType = sourceParam.valueType; +// hasChanges = true; +// } +// } +// } + +// if (hasChanges) +// { +// EditorUtility.SetDirty(preset); +// Debug.Log($"Updated parameters in preset '{preset.Name}' to match source parameters file"); +// } + +// parametersLoaded = true; +// } + +// public override void OnInspectorGUI() +// { +// // Ensure parameters are loaded when inspector is drawn +// LoadMissingParameters(); + +// // Draw the default inspector +// DrawDefaultInspector(); + +// // Custom parameter list rendering +// if (preset.Parameters != null && preset.Parameters.Count > 0) +// { +// EditorGUILayout.Space(); +// EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel); + +// for (int i = 0; i < preset.Parameters.Count; i++) +// { +// var param = preset.Parameters[i]; +// EditorGUILayout.BeginHorizontal(); + +// // Parameter name +// EditorGUILayout.LabelField(param.name, GUILayout.Width(150)); + +// // Parameter type (read-only) +// EditorGUILayout.LabelField($"Type: {param.valueType}", GUILayout.Width(100)); + +// // Should change toggle +// param.shouldChange = EditorGUILayout.Toggle(param.shouldChange, GUILayout.Width(60)); + +// // Set to value (only show if shouldChange is true) +// if (param.shouldChange) +// { +// switch (param.valueType) +// { +// case VRCExpressionParameters.ValueType.Bool: +// param.setTo = EditorGUILayout.Toggle(param.setTo > 0.5f) ? 1f : 0f; +// break; +// case VRCExpressionParameters.ValueType.Int: +// param.setTo = EditorGUILayout.IntField((int)param.setTo); +// break; +// case VRCExpressionParameters.ValueType.Float: +// param.setTo = EditorGUILayout.FloatField(param.setTo); +// break; +// } +// } +// else +// { +// EditorGUILayout.LabelField("(unchanged)", GUILayout.Width(80)); +// } + +// EditorGUILayout.EndHorizontal(); +// } +// } + +// // Add a button to manually reload parameters +// EditorGUILayout.Space(); +// if (GUILayout.Button("Reload Parameters from Group")) +// { +// parametersLoaded = false; +// LoadMissingParameters(); +// } + +// // Add a button to clear all parameters +// if (GUILayout.Button("Clear All Parameters")) +// { +// if (EditorUtility.DisplayDialog("Clear Parameters", +// "Are you sure you want to clear all parameters from this preset?", +// "Yes", "No")) +// { +// preset.Parameters.Clear(); +// EditorUtility.SetDirty(preset); +// } +// } +// } +// } +// } \ No newline at end of file diff --git a/Music Player/Editor/MusicPlayerInspector.cs.meta b/Music Player/Editor/MusicPlayerInspector.cs.meta new file mode 100644 index 0000000..2d94f1f --- /dev/null +++ b/Music Player/Editor/MusicPlayerInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d3599e974bb2eb47819dce224ab2ec9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Runtime.meta b/Music Player/Runtime.meta new file mode 100644 index 0000000..b552978 --- /dev/null +++ b/Music Player/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f080110456f2eaa4db1320833ad91723 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Runtime/MusicPlayerRuntime.asmdef b/Music Player/Runtime/MusicPlayerRuntime.asmdef new file mode 100644 index 0000000..5849cf7 --- /dev/null +++ b/Music Player/Runtime/MusicPlayerRuntime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "MusicPlayerRuntime", + "rootNamespace": "", + "references": [ + "VRC.SDK3A" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Music Player/Runtime/MusicPlayerRuntime.asmdef.meta b/Music Player/Runtime/MusicPlayerRuntime.asmdef.meta new file mode 100644 index 0000000..2102051 --- /dev/null +++ b/Music Player/Runtime/MusicPlayerRuntime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e7fd572735456df489badd3662a8acf4 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Music Player/Runtime/MusicPlayerSong.cs b/Music Player/Runtime/MusicPlayerSong.cs new file mode 100644 index 0000000..c58ea9e --- /dev/null +++ b/Music Player/Runtime/MusicPlayerSong.cs @@ -0,0 +1,14 @@ + +using System.Collections.Generic; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDKBase; +namespace gay.lilyy.MusicPlayer +{ +[RequireComponent(typeof(AudioSource))] +public class MusicPlayerSong : MonoBehaviour, IEditorOnly { + public string Name = "Song Name"; + public int TrackNumber = 1; + public AudioClip song; + } +} \ No newline at end of file diff --git a/Music Player/Runtime/MusicPlayerSong.cs.meta b/Music Player/Runtime/MusicPlayerSong.cs.meta new file mode 100644 index 0000000..3bba72a --- /dev/null +++ b/Music Player/Runtime/MusicPlayerSong.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5491c24bf16b86d45accbad9d16d0522 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: