diff --git a/Assets/Development.meta b/Assets/Development.meta new file mode 100644 index 0000000..92913ef --- /dev/null +++ b/Assets/Development.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d5b152fe6b924475b99c39949a6943d7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest.meta b/Assets/Development/WebGLLoopTest.meta new file mode 100644 index 0000000..6fd0eb4 --- /dev/null +++ b/Assets/Development/WebGLLoopTest.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be51cee13a0a444a08fc1748ad41b75e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef b/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef new file mode 100644 index 0000000..0d3492c --- /dev/null +++ b/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef @@ -0,0 +1,17 @@ +{ + "name": "AudioConductor.Development", + "rootNamespace": "AudioConductor.Development", + "references": [ + "AudioConductor", + "UnityEngine.UI" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef.meta b/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef.meta new file mode 100644 index 0000000..b068071 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/AudioConductor.Development.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 74c1e60e135ef420fb9d01d350682904 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/BgmLoopTest.cs b/Assets/Development/WebGLLoopTest/BgmLoopTest.cs new file mode 100644 index 0000000..3c2e12a --- /dev/null +++ b/Assets/Development/WebGLLoopTest/BgmLoopTest.cs @@ -0,0 +1,113 @@ +// -------------------------------------------------------------- +// Copyright 2026 CyberAgent, Inc. +// -------------------------------------------------------------- + +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; +using AudioConductor.Core; +using AudioConductor.Core.Models; +using AudioConductor.Core.Providers; +using UnityEngine; + +namespace AudioConductor.Development +{ + // Dev-only WebGL loop verification scene; not part of the distributed package sample. + public class BgmLoopTest : MonoBehaviour + { + // One cue per AudioClipLoadType, all sharing the same intro + partial-loop + // settings, so seamless-loop behavior can be compared across load types in one scene. + private const string CueSheetName = "CueSheets/BgmLoopTestCueSheet"; + private static readonly string[] CueNames = { "DecompressOnLoad", "CompressedInMemory", "Streaming" }; + private readonly Dictionary _playbacks = new(); + + private Conductor? _conductor; + private string _log = ""; + private CueSheetHandle _sheetHandle; + private bool _sheetRegistered; + + private void Start() + { + var settings = ScriptableObject.CreateInstance(); + // Each looping cue uses 2 sources for crossover; 3 cues at once need 6, plus margin. + settings.managedPoolCapacity = 8; + _conductor = new Conductor(settings, new ResourcesCueSheetProvider()); + } + + private void OnDestroy() + { + _conductor?.Dispose(); + } + + private void OnGUI() + { + GUILayout.BeginArea(new Rect(20, 20, 460, 700)); + + GUILayout.Label("BGM Loop Test — loadType comparison (intro + partial loop)"); + GUILayout.Space(10); + + foreach (var cueName in CueNames) + { + var playing = _playbacks.ContainsKey(cueName); + + GUILayout.BeginHorizontal(); + GUILayout.Label(cueName + (playing ? " [playing]" : ""), GUILayout.Width(230)); + if (GUILayout.Button(playing ? "■ Stop" : "▶ Play", GUILayout.Height(44))) + { + if (playing) + StopCue(cueName); + else + _ = PlayCue(cueName); + } + + GUILayout.EndHorizontal(); + GUILayout.Space(6); + } + + GUILayout.Space(10); + if (GUILayout.Button("■ Stop All")) + StopAll(); + + GUILayout.Space(10); + GUILayout.Label(_log); + GUILayout.EndArea(); + } + + private async Task PlayCue(string cueName) + { + if (_conductor == null) + return; + + if (!_sheetRegistered) + { + _sheetHandle = await _conductor.RegisterCueSheetAsync(CueSheetName); + _sheetRegistered = true; + _log = "Sheet registered."; + } + + var handle = _conductor.Play(_sheetHandle, cueName, new PlayOptions { IsLoop = true }); + _playbacks[cueName] = handle; + _log += "\nPlay: " + cueName; + } + + private void StopCue(string cueName) + { + if (_conductor == null) + return; + + if (!_playbacks.TryGetValue(cueName, out var handle)) + return; + + _conductor.Stop(handle); + _playbacks.Remove(cueName); + _log += "\nStop: " + cueName; + } + + private void StopAll() + { + foreach (var cueName in new List(_playbacks.Keys)) + StopCue(cueName); + } + } +} diff --git a/Assets/Development/WebGLLoopTest/BgmLoopTest.cs.meta b/Assets/Development/WebGLLoopTest/BgmLoopTest.cs.meta new file mode 100644 index 0000000..c464d9e --- /dev/null +++ b/Assets/Development/WebGLLoopTest/BgmLoopTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9947b7e94567042f689f0c3d468f72f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity b/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity new file mode 100644 index 0000000..a630ccc --- /dev/null +++ b/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity @@ -0,0 +1,266 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &1326202719 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1326202721} + - component: {fileID: 1326202720} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &1326202720 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1326202719} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1326202721 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1326202719} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1833991659 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1833991661} + - component: {fileID: 1833991660} + - component: {fileID: 1833991662} + m_Layer: 0 + m_Name: BgmLoopTest + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1833991660 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1833991659} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9947b7e94567042f689f0c3d468f72f7, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!4 &1833991661 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1833991659} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!81 &1833991662 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1833991659} + m_Enabled: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1326202721} + - {fileID: 1833991661} diff --git a/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity.meta b/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity.meta new file mode 100644 index 0000000..f36f0b4 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 532a82bc171d2424db525b71b62154ab +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Resources.meta b/Assets/Development/WebGLLoopTest/Resources.meta new file mode 100644 index 0000000..763af36 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ec7165657f78f44689bcb3565d2c9a88 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Resources/CueSheets.meta b/Assets/Development/WebGLLoopTest/Resources/CueSheets.meta new file mode 100644 index 0000000..f444102 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Resources/CueSheets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8a7adeeb6d70544749a8fca31c0a87ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset b/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset new file mode 100644 index 0000000..549d77f --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset @@ -0,0 +1,114 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a2a166b4c2c548b991995a3cb1b2885, type: 3} + m_Name: BgmLoopTestCueSheet + m_EditorClassIdentifier: + cueSheet: + name: BgmLoopTestCueSheet + throttleType: 0 + throttleLimit: 0 + volume: 1 + pitch: 1 + pitchInvert: 0 + referenceSampleRate: 44100 + cueList: + - name: DecompressOnLoad + categoryId: 0 + throttleType: 0 + throttleLimit: 3 + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + playType: 0 + cueId: 1 + trackList: + - name: DecompressOnLoad + audioClip: {fileID: 8300000, guid: 7c3c7a7e4fce74155938b4176b33c9a5, type: 3} + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + startSample: 0 + endSample: 349693 + loopStartSample: 149693 + isLoop: 1 + randomWeight: 0 + priority: 0 + fadeTime: 0 + id: 0d7991c9-88d6-4cd1-abcf-d1aefb487f0c + colorId: + id: bba50dbf-8944-4251-b059-f7f9546f963f + colorId: + - name: CompressedInMemory + categoryId: 0 + throttleType: 0 + throttleLimit: 3 + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + playType: 0 + cueId: 2 + trackList: + - name: CompressedInMemory + audioClip: {fileID: 8300000, guid: 1747fd66e075a40d6a80e87e1ec61d21, type: 3} + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + startSample: 0 + endSample: 349693 + loopStartSample: 149693 + isLoop: 1 + randomWeight: 0 + priority: 0 + fadeTime: 0 + id: 5a1422d5-52ea-49e0-b575-b0e822008bac + colorId: + id: 91abd040-ad35-4952-b484-023c91d9a653 + colorId: + - name: Streaming + categoryId: 0 + throttleType: 0 + throttleLimit: 3 + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + playType: 0 + cueId: 3 + trackList: + - name: Streaming + audioClip: {fileID: 8300000, guid: d24bc86cc1f7b4872b08583c0cbdb8b8, type: 3} + volume: 1 + volumeRange: 0 + pitch: 1 + pitchRange: 0 + pitchInvert: 0 + startSample: 0 + endSample: 349693 + loopStartSample: 149693 + isLoop: 1 + randomWeight: 0 + priority: 0 + fadeTime: 0 + id: 8efce089-c99e-4f3e-825e-d914e9a791c4 + colorId: + id: 09e87886-6490-4e82-8839-6542e19b9f16 + colorId: + id: 4fb859b0-418f-4de3-b3f1-2dd424b43473 diff --git a/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset.meta b/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset.meta new file mode 100644 index 0000000..bdb8bfb --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Resources/CueSheets/BgmLoopTestCueSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae70025592dc54acc8f4d4e172e3d597 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Sound.meta b/Assets/Development/WebGLLoopTest/Sound.meta new file mode 100644 index 0000000..970662f --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Sound.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cc5561824dbfe4c2dbdf7175fbab09ef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav new file mode 100644 index 0000000..1bbd741 Binary files /dev/null and b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav differ diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav.meta b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav.meta new file mode 100644 index 0000000..d65a673 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_compressed.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 1747fd66e075a40d6a80e87e1ec61d21 +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 1 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav new file mode 100644 index 0000000..1bbd741 Binary files /dev/null and b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav differ diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav.meta b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav.meta new file mode 100644 index 0000000..ad4f31a --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_decompress.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7c3c7a7e4fce74155938b4176b33c9a5 +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav new file mode 100644 index 0000000..1bbd741 Binary files /dev/null and b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav differ diff --git a/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav.meta b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav.meta new file mode 100644 index 0000000..3b65398 --- /dev/null +++ b/Assets/Development/WebGLLoopTest/Sound/sample_bgm_loop_streaming.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: d24bc86cc1f7b4872b08583c0cbdb8b8 +AudioImporter: + externalObjects: {} + serializedVersion: 7 + defaultSettings: + serializedVersion: 2 + loadType: 2 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..c83ce9a --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 592a26edd31e1424382171297b4e6607 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/WebGLBuildScript.cs b/Assets/Editor/WebGLBuildScript.cs new file mode 100644 index 0000000..bdf3af1 --- /dev/null +++ b/Assets/Editor/WebGLBuildScript.cs @@ -0,0 +1,29 @@ +// -------------------------------------------------------------- +// Copyright 2026 CyberAgent, Inc. +// -------------------------------------------------------------- + +using System.IO; +using UnityEditor; +using UnityEngine; + +public static class WebGLBuildScript +{ + [MenuItem("Build/WebGL Sample")] + public static void BuildWebGL() + { + var outputPath = Path.Combine(Path.GetTempPath(), "AudioConductorWebGL"); + if (Directory.Exists(outputPath)) + Directory.Delete(outputPath, true); + + var options = new BuildPlayerOptions + { + scenes = new[] { "Assets/Development/WebGLLoopTest/BgmLoopTestScene.unity" }, + locationPathName = outputPath, + target = BuildTarget.WebGL, + options = BuildOptions.Development + }; + + var report = BuildPipeline.BuildPlayer(options); + Debug.Log($"Build result: {report.summary.result}"); + } +} diff --git a/Assets/Editor/WebGLBuildScript.cs.meta b/Assets/Editor/WebGLBuildScript.cs.meta new file mode 100644 index 0000000..4f4d196 --- /dev/null +++ b/Assets/Editor/WebGLBuildScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c37a614b6f34400a9ab20a6322aa6dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/AudioConductor/CHANGELOG.md b/Packages/AudioConductor/CHANGELOG.md index 96a658a..742c483 100644 --- a/Packages/AudioConductor/CHANGELOG.md +++ b/Packages/AudioConductor/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v2.4.0 - 2026/06/26 + +- New Features + - Add `referenceSampleRate` to CueSheet: sample positions are automatically scaled at runtime when the clip's actual decoding frequency differs from the authored rate, preventing playback drift on platforms that resample audio (e.g. when Audio Import Settings do not use Preserve Sample Rate); the editor warns when `referenceSampleRate` is unset (Inspector, validation window, and build) and shows a one-time migration dialog on startup to help set it on existing CueSheets + - Seamless BGM loop and reliable scheduled playback on WebGL: Unity's `AudioSource.SetScheduledEndTime` does not work on WebGL even in versions where the reported bug is marked as fixed, and Unity exposes no API to correlate a C# AudioSource with its underlying WebAudio channel. AudioConductor intercepts Unity's internal channel-creation hook to establish the binding and manages loop points and scheduled stops directly via WebAudio. DecompressOnLoad uses native WebAudio loop; CompressedInMemory and Streaming detect their backend at runtime and apply native loop or a crossover strategy accordingly. Pause/resume on app focus loss and system interruptions is also handled under this layer. +- Fix Issues + - Fix out-of-range sample position assignment when setting up a cue with loop points + ## v2.3.1 - 2026/06/11 - Fix Issues diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs index 7df6d83..5edb12a 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs @@ -4,6 +4,8 @@ #nullable enable +using System.Collections.Generic; +using System.Linq; using AudioConductor.Core.Enums; using AudioConductor.Core.Models; using AudioConductor.Core.Shared; @@ -18,16 +20,67 @@ namespace AudioConductor.Editor.Core.Tools.CueSheetEditor.Models internal sealed class CueSheetParameterPaneModel : ICueSheetParameterPaneModel { private readonly IAssetSaveService _assetSaveService; + private readonly ObservableProperty _canApplyReferenceSampleRate; private readonly AutoIncrementHistory _history; + private readonly CueSheet _rawCueSheet; private readonly ObservableCueSheet _target; public CueSheetParameterPaneModel([NotNull] CueSheet cueSheet, [NotNull] AutoIncrementHistory history, [NotNull] IAssetSaveService assetSaveService) { + _rawCueSheet = cueSheet; _target = new ObservableCueSheet(cueSheet); _history = history; _assetSaveService = assetSaveService; + _canApplyReferenceSampleRate = new ObservableProperty(CanApplyReferenceSampleRate); + } + + public IReadOnlyObservableProperty ReferenceSampleRateObservable => _target.ReferenceSampleRateObservable; + + public bool CanApplyReferenceSampleRate => CollectClipFrequencies().Count == 1; + + public IReadOnlyObservableProperty CanApplyReferenceSampleRateObservable => _canApplyReferenceSampleRate; + + public void NotifyClipsChanged() + { + _canApplyReferenceSampleRate.Value = CanApplyReferenceSampleRate; + } + + public void ApplyReferenceSampleRate() + { + var frequencies = CollectClipFrequencies(); + if (frequencies.Count != 1) + return; + var frequency = frequencies.First(); + var old = _target.ReferenceSampleRate; + _history.Register($"Set CueSheet {nameof(CueSheet.referenceSampleRate)} {frequency}", Redo, Undo); + + #region LocalMethods + + void Redo() + { + _target.ReferenceSampleRate = frequency; + _assetSaveService.Save(); + } + + void Undo() + { + _target.ReferenceSampleRate = old; + _assetSaveService.Save(); + } + + #endregion + } + + private HashSet CollectClipFrequencies() + { + var frequencies = new HashSet(); + foreach (var cue in _rawCueSheet.cueList) + foreach (var track in cue.trackList) + if (track.audioClip != null) + frequencies.Add(track.audioClip.frequency); + return frequencies; } #region Name diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs index 3fb8dc0..da48aac 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs @@ -34,5 +34,15 @@ internal interface ICueSheetParameterPaneModel bool PitchInvert { get; set; } IReadOnlyObservableProperty PitchInvertObservable { get; } + + IReadOnlyObservableProperty ReferenceSampleRateObservable { get; } + + bool CanApplyReferenceSampleRate { get; } + + IReadOnlyObservableProperty CanApplyReferenceSampleRateObservable { get; } + + void NotifyClipsChanged(); + + void ApplyReferenceSampleRate(); } } diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs index e09f1d4..c84e501 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs @@ -18,6 +18,7 @@ internal sealed class ObservableCueSheet private readonly ObservableProperty _name; private readonly ObservableProperty _pitch; private readonly ObservableProperty _pitchInvert; + private readonly ObservableProperty _referenceSampleRate; private readonly ObservableProperty _throttleLimit; private readonly ObservableProperty _throttleType; private readonly ObservableProperty _volume; @@ -33,11 +34,20 @@ public ObservableCueSheet([NotNull] CueSheet cueSheet) _volume = new(_cueSheet.volume); _pitch = new(_cueSheet.pitch); _pitchInvert = new(_cueSheet.pitchInvert); + _referenceSampleRate = new(_cueSheet.referenceSampleRate); // ReSharper enable ArrangeObjectCreationWhenTypeNotEvident } public string Id => _cueSheet.Id; + public int ReferenceSampleRate + { + get => _referenceSampleRate.Value; + set => _referenceSampleRate.SetValueAndNotify(_cueSheet.referenceSampleRate = value); + } + + public IReadOnlyObservableProperty ReferenceSampleRateObservable => _referenceSampleRate; + public string Name { get => _name.Value; diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueListEditorPanePresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueListEditorPanePresenter.cs index d3df7e8..d0b4504 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueListEditorPanePresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueListEditorPanePresenter.cs @@ -4,6 +4,7 @@ #nullable enable +using System; using AudioConductor.Editor.Core.Tools.CueSheetEditor.Models.Interfaces; using AudioConductor.Editor.Core.Tools.CueSheetEditor.Views; using AudioConductor.Editor.Foundation.TinyRx; @@ -40,6 +41,8 @@ internal CueListEditorPanePresenter(ICueListEditorPaneModel model, ICueListEdito _cueListPresenter = cueListPresenter; } + public event Action? TrackClipChanged; + public void Dispose() { _cueListPresenter.Dispose(); @@ -125,13 +128,24 @@ private void CleanupViewEventHandlers() private void SetupEventHandlers() { if (_inspectorPresenter is not null) + { _cueListPresenter.OnSelectionItemChanged += _inspectorPresenter.SetModel; + _inspectorPresenter.TrackClipChanged += OnTrackClipChanged; + } } private void CleanupEventHandlers() { if (_inspectorPresenter is not null) + { _cueListPresenter.OnSelectionItemChanged -= _inspectorPresenter.SetModel; + _inspectorPresenter.TrackClipChanged -= OnTrackClipChanged; + } + } + + private void OnTrackClipChanged() + { + TrackClipChanged?.Invoke(); } } } diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetEditorPresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetEditorPresenter.cs index 2b994a3..d71232a 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetEditorPresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetEditorPresenter.cs @@ -107,11 +107,18 @@ private void SetupViewEventHandlers() _view.TabSelectedAsObservable .Subscribe(index => { _model.ObservablePaneState.Value = (Pane)index; }) .DisposeWith(_viewEventDisposable); + _cueListEditorPanePresenter.TrackClipChanged += OnTrackClipChanged; } private void CleanupViewEventHandlers() { _viewEventDisposable.Clear(); + _cueListEditorPanePresenter.TrackClipChanged -= OnTrackClipChanged; + } + + private void OnTrackClipChanged() + { + _model.CueSheetParameterPaneModel.NotifyClipsChanged(); } private void OnSelectPane(Pane pane) diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs index 6c8c643..5864059 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs @@ -68,6 +68,12 @@ private void Bind() _model.PitchInvertObservable .Subscribe(_view.SetPitchInvert) .DisposeWith(_bindDisposable); + _model.ReferenceSampleRateObservable + .Subscribe(_view.SetReferenceSampleRate) + .DisposeWith(_bindDisposable); + _model.CanApplyReferenceSampleRateObservable + .Subscribe(_view.SetApplyButtonEnabled) + .DisposeWith(_bindDisposable); } private void Unbind() @@ -95,6 +101,9 @@ private void SetupViewEventHandlers() _view.PitchInvertChangedAsObservable .Subscribe(value => _model.PitchInvert = value) .DisposeWith(_viewEventDisposable); + _view.ApplyReferenceSampleRateAsObservable + .Subscribe(_ => _model.ApplyReferenceSampleRate()) + .DisposeWith(_viewEventDisposable); } private void CleanupViewEventHandlers() diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/ICueListEditorPanePresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/ICueListEditorPanePresenter.cs index 16df880..0141196 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/ICueListEditorPanePresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/ICueListEditorPanePresenter.cs @@ -4,10 +4,14 @@ #nullable enable +using System; + namespace AudioConductor.Editor.Core.Tools.CueSheetEditor.Presenters { internal interface ICueListEditorPanePresenter : ICueSheetEditorPanePresenter { + event Action? TrackClipChanged; + void FocusCue(string cueEditorId); } } diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/InspectorPresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/InspectorPresenter.cs index 184ca0c..0a829d8 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/InspectorPresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/InspectorPresenter.cs @@ -27,15 +27,19 @@ public InspectorPresenter(InspectorView view) _cueTrackInspectorPresenter = new CueTrackInspectorPresenter(_view.Q()); _cueInspectorPresenter = new CueInspectorPresenter(_view.Q()); _trackInspectorPresenter = new TrackInspectorPresenter(_view.Q()); + _trackInspectorPresenter.ClipChanged += OnTrackClipChanged; } public void Dispose() { + _trackInspectorPresenter.ClipChanged -= OnTrackClipChanged; _trackInspectorPresenter.Dispose(); _cueInspectorPresenter.Dispose(); _view.Dispose(); } + public event Action? TrackClipChanged; + public void Setup() { _view.Setup(); @@ -64,6 +68,11 @@ public void SetModel(IInspectorModel model) } } + private void OnTrackClipChanged() + { + TrackClipChanged?.Invoke(); + } + private void CloseAllInspector() { _cueTrackInspectorPresenter.Close(); diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/TrackInspectorPresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/TrackInspectorPresenter.cs index eb30988..9ba93d4 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/TrackInspectorPresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/TrackInspectorPresenter.cs @@ -35,6 +35,8 @@ public void Dispose() _view.Dispose(); } + public event Action? ClipChanged; + public void Setup() { _view.Setup(); @@ -72,6 +74,9 @@ private void Bind() model.AudioClipObservable .Subscribe(_view.SetAudioClip) .DisposeWith(_bindDisposable); + model.AudioClipObservable + .Subscribe(_ => ClipChanged?.Invoke()) + .DisposeWith(_bindDisposable); model.AudioClipObservable .Subscribe(_view.SetSampleRange) .DisposeWith(_bindDisposable); diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs index f8207df..acc1feb 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs @@ -15,12 +15,16 @@ namespace AudioConductor.Editor.Core.Tools.CueSheetEditor.Views { internal sealed class CueSheetParameterPaneView : VisualElement, IDisposable { + private readonly Button _applyReferenceSampleRateButton; + private readonly Subject _applyReferenceSampleRateSubject = new(); private readonly Subject _nameChangedSubject = new(); private readonly TextField _nameField; private readonly Subject _pitchChangedSubject = new(); private readonly SliderAndFloatField _pitchField; private readonly Subject _pitchInvertChangedSubject = new(); private readonly Toggle _pitchInvertField; + private readonly IntegerField _referenceSampleRateField; + private readonly HelpBox _referenceSampleRateWarning; private readonly Subject _throttleLimitChangedSubject = new(); private readonly IntegerField _throttleLimitField; private readonly Subject _throttleTypeChangedSubject = new(); @@ -45,6 +49,13 @@ public CueSheetParameterPaneView() _pitchField.lowValue = ValueRangeConst.Pitch.Min; _pitchField.highValue = ValueRangeConst.Pitch.Max; + _referenceSampleRateField = this.Q("ReferenceSampleRate"); + _referenceSampleRateField.SetEnabled(false); + _referenceSampleRateWarning = this.Q("ReferenceSampleRateWarning"); + _referenceSampleRateWarning.SetDisplay(false); + _applyReferenceSampleRateButton = this.Q