using NAudio.Dsp; using NAudio.Midi; using QuikDawEditor.SampleProviders; using System.Text.Json.Serialization; namespace QuikDawEditor.EditingClasses; public partial class Track : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; internal void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public string RelRes() { ReleaseResources(); return "Resources released"; } public VstiWindow vstiWin; public VstFxWindow fxWindow; //public FXWindow fxWindow; public string UndoTrackID { get; set; } //For Haas effect, use 5 ms, = (5/200 ms = .025) * 8820 = approx. 220 samples delayed internal List HaasBuffer = new List(new float[0]); private double _HaasDelay = 0; public double HaasDelay { get { return _HaasDelay; } set { _HaasDelay = value; NotifyPropertyChanged(nameof(HaasDelay)); this.HaasBuffer = new List(new float[delayedSamples]); } } private int delayedSamples { get { return (int)(_HaasDelay * 44.1D); } } internal int preMoveIndex = -1; private string _TrackName { get; set; } = "New track"; public string TrackName { get { return _TrackName; } set { _TrackName = value; NotifyPropertyChanged(nameof(TrackName)); } } public string RecordInDeviceName { get { if (editingProject == null || editingProject.RecordInputs == null || editingProject.RecordInputs.Count == 0) return ""; else return trackType == TrackType.Audio ? editingProject.RecordInputs[Settings.Default.RecordInIndex].RecordDeviceName + " " + editingProject.RecordInputs[Settings.Default.RecordInIndex].RecordInputName : (MidiIn.NumberOfDevices == 0 ? "No midi device" : MidiIn.DeviceInfo(currentMidiInDeviceNo).ProductName); } } public void UpdateRecInToolTip() { NotifyPropertyChanged(nameof(RecordInDeviceName)); } private string _ToolTipText = ""; private Point _ToolTipDisplayPoint; [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public Point ToolTipDisplayPoint { get { return _ToolTipDisplayPoint; } set { _ToolTipDisplayPoint = value; NotifyPropertyChanged(nameof(ToolTipMargin)); } } [JsonIgnore(Condition = JsonIgnoreCondition.Always)] public string ToolTipText { get { return _ToolTipText; } set { _ToolTipText = value; NotifyPropertyChanged(nameof(ToolTipText));NotifyPropertyChanged(nameof(ToolTipLabelVisibility)); } } public Visibility ToolTipLabelVisibility { get { return ToolTipText == "" ? Visibility.Hidden : Visibility.Visible; } } //public Thickness ToolTipMargin { get { return new Thickness(ToolTipDisplayPoint.X, ToolTipDisplayPoint.Y, 0, 0); } } public Thickness ToolTipMargin { get { return new Thickness(0, 37, 0, 0); } } internal int currentMidiInDeviceNo = 0; internal int currentAudioInDeviceNo = 0; public bool DragOverAutomationLane = false; public bool ArmButEnabled { get { return projPlayer.AudioDeviceOK && projPlayer.IsProjectNotPlaying; } } public void UpdateArmButEnabled() { NotifyPropertyChanged(nameof(ArmButEnabled)); } public bool IsMidiTrack { get { return trackType == TrackType.Midi; } } public bool IsAudioTrack { get { return trackType == TrackType.Audio; } } public bool IsSubmixTrack { get { return trackType == TrackType.Submix; } } private TrackType _trackType; public TrackType trackType { get { return _trackType; } set { _trackType = value; NotifyPropertyChanged(nameof(DisplayTrackBackgroundBrush)); NotifyPropertyChanged(nameof(IsMidiTrack)); } } public Color TrackContentControlBackgroundColor { get { return trackType == TrackType.Submix ? Colors.White : Colors.Transparent; } } //public Color TrackContentControlBackgroundColor { get { return trackType switch { TrackType.Submix => Colors.White, _ => Colors.Transparent }; } } private Color _TrackBackgroundColor; public Color TrackBackgroundColor { get { return _TrackBackgroundColor; } set { _TrackBackgroundColor = value; NotifyPropertyChanged(nameof(DisplayTrackBackgroundBrush)); if (!this.IsSubmixTrack) foreach (Clip c in this.Clips) c.UpdateClipTopLabelColor(); } } public SolidColorBrush TrackForegroundBrush { get { return trackType == TrackType.Submix ? Brushes.White : new SolidColorBrush(Color.FromArgb(255, 20, 20, 20)); } } public double TrackNameShadowOpacity { get { return trackType == TrackType.Submix ? 1 : 0; } } public LinearGradientBrush DisplayTrackBackgroundBrush { get { switch (IsRecordingArmed) { case true: Color leftColor = Colors.Gainsboro; Color midColor = Colors.Crimson; Color rightColor = Colors.Red; return new LinearGradientBrush() { GradientStops = new GradientStopCollection() { new GradientStop (leftColor, 0.0), new GradientStop(midColor, 0.7), new GradientStop(rightColor, 1.0) } }; case false: return TrackBackgroundBrush; } } } public LinearGradientBrush TrackBackgroundBrush { get { Color leftColor, midColor, rightColor; leftColor = Color.FromRgb(Convert.ToByte(Math.Min(255, _TrackBackgroundColor.R * 1.2)), Convert.ToByte(Math.Min(255, _TrackBackgroundColor.G * 1.2)), Convert.ToByte(Math.Min(255, _TrackBackgroundColor.B * 1.2))); midColor = Color.FromRgb(Convert.ToByte(_TrackBackgroundColor.R / 1.2), Convert.ToByte(_TrackBackgroundColor.G / 1.2), Convert.ToByte(_TrackBackgroundColor.B / 1.2)); rightColor = Color.FromRgb(Convert.ToByte(_TrackBackgroundColor.R / 1.6), Convert.ToByte(_TrackBackgroundColor.G / 1.6), Convert.ToByte(_TrackBackgroundColor.B / 1.6)); LinearGradientBrush returnBrush = new LinearGradientBrush() { GradientStops = new GradientStopCollection() { new GradientStop (leftColor, 0.0), new GradientStop(midColor, 0.5), new GradientStop(rightColor, 1.0) } }; foreach (AutomationLane autoclip in this.AutomationLanes) autoclip.AutoClipBackgroundBrush = new LinearGradientBrush(returnBrush.GradientStops, new Point(0, 1), new Point(0, 0)) { Opacity = 0.85 }; return returnBrush; } } private float _Volume = 0; public float Volume { get { return _Volume; } set { _Volume = value; NotifyPropertyChanged(nameof(VolumeDB)); } } private bool _IsMuted = false; public bool IsMuted { get { return _IsMuted; } set { _IsMuted = value; NotifyPropertyChanged(nameof(IsMuted)); NotifyPropertyChanged(nameof(GetsPlayed)); if (value == true) TrackOutputVolume = 0; } } private bool _IsSoloed = false; public bool IsSoloed { get { return _IsSoloed; } set { _IsSoloed = value; NotifyPropertyChanged(nameof(IsSoloed)); NotifyPropertyChanged(nameof(GetsPlayed)); } } private bool _IsSoloMuted = false; public bool IsSoloMuted { get { return _IsSoloMuted; } set { _IsSoloMuted = value; NotifyPropertyChanged(nameof(GetsPlayed)); if (value == true) TrackOutputVolume = 0; } } private bool _IsTrackCollapsed = false; public bool IsTrackCollapsed { get { return _IsTrackCollapsed; } set { _IsTrackCollapsed = value; NotifyPropertyChanged(nameof(IsTrackCollapsed)); NotifyPropertyChanged(nameof(TrackVisibility)); NotifyPropertyChanged(nameof(GetsPlayed)); } } private bool _SubTracksVisible = true; public bool SubTracksVisible { get { return _SubTracksVisible; } set { _SubTracksVisible = value; NotifyPropertyChanged(nameof(SubTracksVisible)); NotifyPropertyChanged(nameof(ToggleSubmixButtonImage)); } } public string ToggleSubmixButtonImage { get { return _SubTracksVisible ? "pack://application:,,,/QuikDawEditor;component/EDITING/images/ToggleMinus.png" : "pack://application:,,,/QuikDawEditor;component/EDITING/images/TogglePlus.png"; } } private double _TrackHeight = 80; public double TrackHeight { get { return trackType == TrackType.Submix ? 74 : _TrackHeight; } set { _TrackHeight = value; NotifyPropertyChanged(nameof(TrackHeight)); foreach (Clip c in Clips) c.UpdateClipHeightY(); } } public double GetActualTrackHeight { get { return TrackHeight + (AutomationLanesVisible ? 60 : 0); } } double trackTop = 0; public double GetActualTrackTop { get { trackTop = 0; CalculateTrackTop(editingProject.Tracks); return trackTop; } } public Thickness TrackContentControlBorderThickness { get { return trackType == TrackType.Submix ? new Thickness(0, 2, 0, 0) : new Thickness(0, 0, 0, 1.15); } } private bool CalculateTrackTop(ObservableCollection tracks) { foreach (Track t in tracks) { if (t == this) return true; if (!editingProject.hiddenTracks.Contains(t)) { trackTop += (t.GetActualTrackHeight * t.TrackYScale); if (t.IsSubmixTrack && t.SubTracksVisible && !t.ContentHidden) if (CalculateTrackTop(t.SubTracks)) return true; } } return false; } internal Track parentTrack; internal Track premovingParentTrack; internal int GetTrackIndex { get { return parentTrack == null ? editingProject.Tracks.IndexOf(this) : parentTrack.SubTracks.IndexOf(this); } } public Visibility TrackVisibility { get { return IsTrackCollapsed ? Visibility.Collapsed : Visibility.Visible;} } public Visibility TrackChildrenVisibility { get { return ContentHidden ? Visibility.Hidden : Visibility.Visible;} } private double _TrackYScale = 1; public double TrackYScale { get { return _TrackYScale; } set { _TrackYScale = value; NotifyPropertyChanged(nameof(TrackYScale)); NotifyPropertyChanged(nameof(ReverseTrackYScale)); foreach (Clip c in this.Clips) c.UpdateClipTopLabelX(_TrackYScale); } } public double ReverseTrackYScale { get { return 1 / _TrackYScale; } } public bool IsPianoRollMuted = false; public bool GetsPlayed { get { return (IsSoloed | !(IsMuted | IsSoloMuted | IsTrackCollapsed)) && !IsPianoRollMuted ; } } public bool hasData { get { return Clips.Count > 0; } } public bool BeyondEnd { get { return this.Clips.Count == 0 || projPlayer.CurrentPlayingPosMS > this.Clips.Last().ClipRightMs; } } public SolidColorBrush TrackTypeIndicatorBorder { get { return IsMidiTrack ? Brushes.Black : Brushes.Transparent; } } private float LPan; private float RPan; private float m_pan = 0; public float Pan { get { return m_pan; } set { if (value < -1.0F || value > 1.0F) throw new ArgumentOutOfRangeException("value", "Pan must be in the range -1 to 1"); m_pan = value; //normPan = (-value + 1) / 2; float adjustedPan = Math.Max(-1, Math.Min(1, m_pan + _panAutomationFactor)); normPan = (-adjustedPan + 1) / 2; LPan = Convert.ToSingle(Math.Sin(normPan * HalfPi)); RPan = Convert.ToSingle(Math.Cos(normPan * HalfPi)); NotifyPropertyChanged(nameof(Pan)); } } private const float HalfPi = 1.570796F; private float normPan; public bool ReceivingAdjustment = false; public bool SettingFilters = false; private bool _IsTrackEQActive = false; public bool IsTrackEQActive { get { return _IsTrackEQActive; } set { _IsTrackEQActive = value; NotifyPropertyChanged(nameof(IsTrackEQActive)); } } private bool _IsHaasDelayActive = false; public bool IsHaasDelayActive { get { return _IsHaasDelayActive; } set { _IsHaasDelayActive = value; NotifyPropertyChanged(nameof(IsHaasDelayActive)); } } public ObservableCollection Clips { get; set; } = new ObservableCollection(); public ObservableCollection AutomationLanes { get; set; } = new ObservableCollection(); public ObservableCollection SubTracks { get; set; } = new ObservableCollection(); public Thickness TrackControlBorderThickness { get { return IsSubmixTrack ? new Thickness(4, 4, 0, 4) : new Thickness(0); } } private void SubTracks_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { if (e.NewItems.Count > 0) foreach (Track t in e.NewItems) t.parentTrack = this; } if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) { if (e.OldItems.Count > 0) foreach (Track t in e.OldItems) t.parentTrack = null; } editingProject.UpdateLeftDPWidth(); } public Thickness TrackMargin { get { return new Thickness( parentTrack == null ? 0 : 7, 0, 0, 0); } } public EqualizerBand[] EQBands { get; set; } = new EqualizerBand[8]; public ObservableCollection TrackEffectVsts { get; set; } = new ObservableCollection(); public ObservableCollection TrackInstrumentVsts { get; set; } = new ObservableCollection(); public IntPair FxWinLocation { get; set; } = new IntPair() { int1 = 400, int2 = 100 }; public void SortAndUpdateClipIndexes() { List sortClipList = new List(Clips); sortClipList.Sort((c1, c2) => c1.ClipLeftMs.CompareTo(c2.ClipLeftMs)); Clips.Clear(); foreach (Clip c in sortClipList) Clips.Add(c); UpdateClipOwner(this); } public void UpdateMyClipsToOwner() { UpdateClipOwner(this); } public void UpdateClipOwner(Track owningTrack) { for (int clipno = 0; clipno < owningTrack.Clips.Count; clipno++) owningTrack.Clips[clipno].myTrack = owningTrack; if (owningTrack.IsSubmixTrack) foreach (Track strack in owningTrack.SubTracks) strack.UpdateClipOwner(strack); } internal TrackSampleProvider myTrackSampleProvider; //public Clip CurrentPlayingClip { get { Clip returnClip = null; try { returnClip = Clips.FirstOrDefault(c => c.IsCurrentClip); } catch {Debug.WriteLine("no current clip"); } return returnClip; } } public Clip GetCurrentPlayingClip { get { return Clips.FirstOrDefault(c => c.IsCurrentClip); } } //public Clip NextPlayingClip { get { return Clips.FirstOrDefault(c => c.IsNextClip); } } public Clip NextPlayingClip { get { return (GetCurrentPlayingClip == null || GetCurrentPlayingClip.GetIndexInTrack == Clips.Count -1) ? null : this.Clips[GetCurrentPlayingClip.GetIndexInTrack + 1]; } } public Clip LastCurrentClip; internal void ResetAllSubClips(Track thisTrack, double msPoint) { thisTrack.ResetClips(msPoint); if (thisTrack.IsSubmixTrack) foreach (Track strack in thisTrack.SubTracks) strack.ResetAllSubClips(strack, msPoint); } internal void InsertClip(int insertIdx, Clip insertClip) { insertClip.myTrack = this; try { if (insertClip.IsAudioClip) { //Create clipsampleprovider for clip this.AddClipSampleProviderToClip(insertClip); insertClip.ApplyWaveBitmapSourcesToImage(); } } catch (Exception ex) { MessageBox.Show("error undoing clip:\n" + ex.Message); } this.Clips.Insert(insertIdx, insertClip); } internal void ResetClips(double msPoint) { //FlushVsts(); if (Clips.Count == 0) return; Clip currClip = GetCurrentPlayingClip; if (currClip == null) { if (NextPlayingClip != null) { if (NextPlayingClip.IsAudioClip) NextPlayingClip.ResetMe(msPoint); LastCurrentClip = NextPlayingClip; } return; } if (currClip.IsAudioClip) currClip.ResetMe(msPoint); if (LastCurrentClip == null || currClip.GetIndexInTrack != LastCurrentClip.GetIndexInTrack) LastCurrentClip = currClip; HaasBuffer = new List(new float[delayedSamples]); } internal List GetAllMidiNoteNosInTrack { get { List returnList = new List(); foreach (Clip c in this.Clips) returnList.AddRange(c.ClipMidiNotes.ToList().ConvertAll(cmn => cmn.Note)); return returnList; } } internal void StopMidiNotes() { foreach (Clip c in this.Clips) c.StopAllPlayingMidiNotes(); } public void ToggleSolo() { this.IsSoloed = !this.IsSoloed; int soloedTracksCount = editingProject.GetAllTracksInProject.Where(t => t.IsSoloed).Count(); List allTracks = editingProject.GetAllTracksInProject; for (int tno = 0; tno < allTracks.Count; tno++) { bool soloMute = !allTracks[tno].IsSoloed && !allTracks[tno].IsSoloed & !allTracks[tno].IsSubmixTrack; if (allTracks[tno].parentTrack != null) soloMute = soloMute && !allTracks[tno].parentTrack.IsSoloed; allTracks[tno].IsSoloMuted = soloedTracksCount > 0 && soloMute; allTracks[tno].ResetClips(projPlayer.CurrentPlayingPosMS); } if (this.parentTrack != null) this.parentTrack.IsSoloMuted = false; if (this.IsSubmixTrack) { this.IsSoloMuted = false; if (this.SubTracks.Where(t => t.IsSoloed).Count() == 0) foreach (Track t in this.SubTracks) t.IsSoloMuted = !this.IsSoloed && allTracks.Where(t=>t.IsSoloed).Count() > 0; } } internal void Clips_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { //Debug.WriteLine("Clips collection was changed"); if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { foreach (Clip newClip in e.NewItems) newClip.myTrack = this; } projPlayer.UpdateCanMixdown(); if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) { foreach (Clip c in e.OldItems) if (editingProject.pianoRoll.myClip == c) { editingProject.pianoRoll.myClip = null; break; } } } internal List UsedClipStreams = new List(); public void AddClipSampleProviderToClip(Clip thisClip) { thisClip.myClipSampleProvider = new ClipSampleProvider(); //Check if this clip's track already has this sourcefile read into wavestream, and if so, use the same stream ClipStream foundClipStream = this.UsedClipStreams.Where(cs => cs.baseFileName == thisClip.ClipSourceFileName).FirstOrDefault(); ////DEBUGGING only, to force each clip to make new clipstream //foundClipStream = null; if (foundClipStream == null) { //Create a new clip stream for this clip source on this track if (File.Exists(thisClip.clipSourceFullPath)) { ClipStream newClipStream = new ClipStream(thisClip.clipSourceFullPath); thisClip.myClipSampleProvider.myClipStream = newClipStream; this.UsedClipStreams.Add(newClipStream); } else { //MessageBox.Sxhow("File not found:::\n" + thisClip.ClipSourceFileName); thisClip.IsOrphanClip = true; return; } } else { //Use the same clip stream as clip on same track with same clip source thisClip.myClipSampleProvider.myClipStream = foundClipStream; } thisClip.myClipSampleProvider.AssignSampleProvider(thisClip.IsReversed); thisClip.WaveFormatString = thisClip.myClipSampleProvider.WaveFormat.ToString(); //thisClip.ResetMe(projPlayer.CurrentPlayingPosMS); Debug.WriteLine("made new clipsampprov"); } internal void AddHandlers() { switch (trackType) { case TrackType.Submix: SubTracks.CollectionChanged += SubTracks_CollectionChanged; break; default: Clips.CollectionChanged += Clips_CollectionChanged; break; } } internal void RemoveAndDisposeClip(Clip deletingClip) { this.Clips.Remove(deletingClip); if (deletingClip.clipType == ClipType.Audio) { // MUST CHECK TO SEE IF OTHER CLIPS USING ITS STREAM, if so, then don't dispose stream bool usedByOtherClip = false; foreach (Clip c in this.Clips) if (c.ClipSourceFileName == deletingClip.ClipSourceFileName) { usedByOtherClip = true; break; } if (!usedByOtherClip) { if (deletingClip.myClipSampleProvider != null) { UsedClipStreams.Remove(deletingClip.myClipSampleProvider.myClipStream); deletingClip.Dispose(); } } } } public WaveFormat WaveFormat = DefaultPlayWaveFormat; public readonly BiQuadFilter[,] filters; private bool updated; private int channels = 2; public int bandCount = 0; public float[] EqualizedSamples(float[] samplesToEqualize, int offset, int outputSamples) { if (updated) { CreateFilters(); updated = false; } for (int n = 0; n < outputSamples; n++) { int ch = n % channels; for (int band = 0; band < bandCount; band++) samplesToEqualize[offset + n] = filters[ch, band].Transform(samplesToEqualize[offset + n]); } return samplesToEqualize; } private void CreateFilters() { for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) { var band = EQBands[bandIndex]; for (int n = 0; n < 2; n++) { if (filters[n, bandIndex] == null) filters[n, bandIndex] = BiQuadFilter.PeakingEQ(WaveFormat.SampleRate, band.Frequency, band.Bandwidth, band.Gain); else filters[n, bandIndex].SetPeakingEq(myTrackSampleProvider.WaveFormat.SampleRate, band.Frequency, band.Bandwidth, band.Gain); } } } public void UpdateFilters() { updated = true; CreateFilters(); } }