Having trouble limiting method access between two classes. Need a new design.

I am working on an audio manager and the idea is this…
I will have a AudioTracks regular system.object class that will have a list (or dictionary) of audio clips as well as information such as volume and audio channel. We use a list because we might have multiple audio clips that share the same settings, and I feel this would be a less annoying way to handle that.
I then have an AudioController MonoBehaviour component that is pretty much just a component that can hold a AudioTracks object as well as setting up an audiosource to be used.
Then there will be a static AudioManager that handles pooling and such. The pool will be pools of gameobjects will AudioControllers, but we have the choice of not using the pool if we just place an AudioController on a gameobject before hand.
The idea is to be able to just pass around the AudioTracks to any AudioController and everything will be set up properly with the new settings.

The issue I am running into is between the AudioController and AudioTracks.
Let me show some code first.
Click for code

//* = Will need to make a custom editor if we want this to work properly at runtime in the editor
public class AudioController : MonoBehaviour
{
    [SerializeField] AudioTrack _audioTrack; //*
    public AudioTrack audioTrack {get{return _audioTrack;} set{SetAudioTrack(value);}}

    AudioSource audioSource;

    void Awake()
    {
        audioSource = gameObject.AddComponent<AudioSource>();
        audioSource.hideFlags = HideFlags.NotEditable | HideFlags.HideInInspector;
        SetAudioTrack(_audioTrack);
    }

    void SetAudioTrack(AudioTrack audioTrack)
    {
        //Here we need to take the parameters from the audiotrack and properly set things up on the audiosource and audiomanager such as volume
    }
}

public class AudioTrack
{
    //Other fields such as the audio clips dictionary....

    [SerializeField] AudioChannel _channel; //*
    public AudioChannel channel {get{return _channel;} set{SetChannel(value);}}

    [Range(0f, 1f)]
    [SerializeField] float _volume; //*
    public float volume {get{return _volume;} set{SetVolume(value);}}

    void SetChannel(AudioChannel channel)
    {
        //We need to set our channel, then tell the audiomanager and give them our audiosource to handle volume, but audiocontroller has our audiosource...
    }

    void SetVolume(float volume)
    {
        //We need to set our volume and then tell audiocontroller to set the audiosource volume properly, but how can we assign things in a way that we know our audiocontroller?
    }
}

So the issue here is, the AudioTrack wants to know about the AudioController or the AudioController wants to know about the AudioTrack, but we cant just pass data through some public method that only the AudioController should use as thats not really good design and can break things.
I wanted to use C# events and let the AudioController assign its private methods to a OnVolumeChange and OnChannelChange event, but assigning to an event causes garbage, and since this will be used in a object pool where things will be constantly assigned and unassigned, I will have to avoid events.
I also dont want to use reflection as I am more curious on how this would be handled normally.
Nesting the AudioTrack class within the AudioController might work, but it would be annoying referencing the AudioTrack as AudioController.AudioTrack (or visa versa) or creating wrappers.

Here is an example of the mess and how I dont like how unsafe it is
Click for code

public class AudioController : MonoBehaviour
{
    [SerializeField] AudioTrack _audioTrack;
    public AudioTrack audioTrack {get{return _audioTrack;} set{SetAudioTrack(value);}}

    AudioSource audioSource;

    void Awake()
    {
        audioSource = gameObject.AddComponent<AudioSource>();
        audioSource.hideFlags = HideFlags.NotEditable | HideFlags.HideInInspector;
        SetAudioTrack(_audioTrack);
    }

    void OnDestroy()
    {
        if(_audioTrack != null) _audioTrack.RemoveFromAudioController();
    }

    void SetAudioTrack(AudioTrack audioTrack)
    {
        if(_audioTrack != null) _audioTrack.RemoveFromAudioController();
        _audioTrack = audioTrack;
        if(_audioTrack != null) _audioTrack.SetAudioController(this, audioSource);
    }
}

public class AudioTrack
{
    //Other fields such as the audio clips dictionary....

    [SerializeField] AudioChannel _channel; //will need to make a custom editor if we want these to work properly at runtime in the editor
    public AudioChannel channel {get{return _channel;} set{SetChannel(value);}}

    [Range(0f, 1f)]
    [SerializeField] float _volume; //will need to make a custom editor if we want these to work properly at runtime in the editor
    public float volume {get{return _volume;} set{SetVolume(value);}}

    AudioSource audioSource;
    AudioController audioController;

    //I dont like how unsafe this method is...
    //How do we know the audiosource is the audiosource the audiocontroller is using, and how do we make sure this audiotrack is the audiotrack on the audiocontroller (without some strange runtime check)?
    public void SetAudioController(AudioController controller, AudioSource source)
    {
        audioSource = source;
        audioController = controller;

        SetVolume(_volume);
        SetChannel(_channel);
    }

    public void RemoveFromAudioController()
    {
        //code for removing from audiomanager goes here

        audioSource = null;
        if(audioController != null) audioController.audioTrack = null;
        audioController = null;
    }

    void SetChannel(AudioChannel channel)
    {
        _channel = channel;

        //tell audiomanager and give them our audiosource to handle volume
    }

    void SetVolume(float volume)
    {
        volume = Mathf.Clamp01(volume);
        if(audioSource != null) audioSource.volume *= (volume / _volume);
        _volume = volume;
    }
}

So I was going for just having the audiotrack handle everything, but the SetAudioController method is a method only the AudioController should be using. If someone calls that method thinking it would properly assign this audiotrack to the audiocontroller, they would be breaking things.
I have not gone to far into designing this, but I think I will have it be so that whenever I want to play an audiotracks clip, I call a play method within the audiotrack, which would check if we are assigned to an audiocontroller and if so then tell the audiocontroller to have the audiosource play the clip, but if we are not assigned to an audiocontroller, then to grab one from the audiomanager pool.

So I am kinda stuck on this and figure its a design flaw and am wondering how to go about handling this.

So I might go with nested classes.
Here is what I got so far…
Click for code

    [Serializable]
    public class AudioTracks
    {
        //more fields...

        [SerializeField] AudioChannel _channel; //will need to make a custom editor if we want these to work properly at runtime in the editor
        public AudioChannel channel {get{return _channel;} set{SetChannel(value);}}
 
        [SerializeField] float _volume;
        public float volume {get{return _volume;} set{SetVolume(value);}}

        AudioController audioController;
        AudioSource audioSource;

        void SetChannel(AudioChannel channel)
        {
            _channel = channel;
     
            //tell audio manager and pass our audiosource
        }

        void SetVolume(float volume)
        {
            volume = Mathf.Clamp01(volume);

            if(audioSource != null) audioSource.volume *= (volume / _volume); //doesnt really work
           //probably need to do something like audioSource.volume = AudioManager.GetChannelVolume(channel) * volume;

            _volume = volume;
        }

        public class AudioController : MonoBehaviour
        {
            [SerializeField] AudioTracks _audioTracks;
            public AudioTracks audioTracks {get{return _audioTracks;} set{SetAudioTrack(value);}}

            AudioSource audioSource;

            void Awake()
            {
                audioSource = gameObject.AddComponent<AudioSource>();
                audioSource.hideFlags = HideFlags.NotEditable | HideFlags.HideInInspector;
                SetAudioTrack(_audioTracks);
            }

            void OnDestroy()
            {
                RemoveAudioTrack(_audioTracks);
            }

            void SetAudioTrack(AudioTracks audioTracks)
            {
                RemoveAudioTrack(_audioTracks); //Remove our current audiotrack from us
                RemoveAudioTrack(audioTracks); //remove our new audiotrack from its old audiocontroller if it was attatched to one

                if(audioTracks != null)
                {
                    audioTracks.audioController = this;
                    audioTracks.audioSource = audioSource;

                    audioTracks.SetChannel(audioTracks.channel);
                    audioTracks.SetVolume(audioTracks.volume);
                }

                _audioTracks = audioTracks;
            }

            void RemoveAudioTrack(AudioTracks audioTracks)
            {
                if(audioTracks != null)
                {
                    //remove from audiomanager

                    if(audioTracks.audioController != null) audioTracks.audioController._audioTracks = null;
                    audioTracks.audioController = null;
                    audioTracks.audioSource = null;
                }
            }
        }
    }

I then have in another file just this

public class AudioController : AudioTracks.AudioController {}

which would be what I use as a the component.

It seems so hacky though. Still curious as to how it could be handled better.

Edit - Me from the future.
I am still doing the nesting trick, except I am doing it a bit differently from above. I ran into a lot of annoying issues with the above when dealing with types, so what I am doing now is something like this…
Click for code

    public class AudioSourceAccess : MonoBehaviour
    {
        protected AudioSource audioSource;

        public class AudioTrackAccess
        {
            protected AudioSource GetSource(AudioController controller)
            {
                return controller.audioSource;
            }
        }
    }

So I basically have those mini classes setup in a way that can give me access to what I need via methods and such. Then I will have my main classes in their own unnested class and just inherit from what ever “Access” class they need.
So my AudioController inherits from AudioSourceAccess and is able to set that audiosource, and my AudioTracks inherits from AudioTrackAccess, which can access my AudioControllers private AudioSource variable by using the GetSource method.

This way all the nested mess is kept separate from my actual classes.