diff --git a/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs b/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs index 785645e..7d786b3 100644 --- a/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs +++ b/src/SoundMaker/Sounds/Score/BasicSoundComponentBase.cs @@ -31,6 +31,8 @@ public BasicSoundComponentBase(LengthType length, bool isDotted) public abstract short[] GenerateWave(SoundFormat format, int tempo, WaveTypeBase waveType); + public abstract ISoundComponent Clone(); + public int GetWaveArrayLength(SoundFormat format, int tempo) { return SoundWaveLengthCalclator.Calclate(format, tempo, Length, IsDotted); diff --git a/src/SoundMaker/Sounds/Score/ISoundComponent.cs b/src/SoundMaker/Sounds/Score/ISoundComponent.cs index bb576bd..da7962a 100644 --- a/src/SoundMaker/Sounds/Score/ISoundComponent.cs +++ b/src/SoundMaker/Sounds/Score/ISoundComponent.cs @@ -37,4 +37,14 @@ public interface ISoundComponent /// data of wave. 波形データ : short[] /// Tempo must be non-negative and greater than 0. short[] GenerateWave(SoundFormat format, int tempo, WaveTypeBase waveType); + + /// + /// Creates a clone of the sound component.
+ /// サウンドコンポーネントのクローンを作成するメソッド。 + ///
+ /// + /// A new instance of the sound component with the same properties.
+ /// 同じプロパティを持つサウンドコンポーネントの新しいインスタンス + ///
+ ISoundComponent Clone(); } diff --git a/src/SoundMaker/Sounds/Score/Note.cs b/src/SoundMaker/Sounds/Score/Note.cs index aa9815c..175f064 100644 --- a/src/SoundMaker/Sounds/Score/Note.cs +++ b/src/SoundMaker/Sounds/Score/Note.cs @@ -92,4 +92,12 @@ public override short[] GenerateWave(SoundFormat format, int tempo, WaveTypeBase var length = GetWaveArrayLength(format, tempo); return GenerateWave(format, tempo, length, waveType); } + + public override Note Clone() + { + return new(Scale, ScaleNumber, Length, IsDotted) + { + Volume = Volume, + }; + } } diff --git a/src/SoundMaker/Sounds/Score/Rest.cs b/src/SoundMaker/Sounds/Score/Rest.cs index 3af7480..f989083 100644 --- a/src/SoundMaker/Sounds/Score/Rest.cs +++ b/src/SoundMaker/Sounds/Score/Rest.cs @@ -4,15 +4,14 @@ namespace SoundMaker.Sounds.Score; /// /// the rest. 休符を表すクラス /// -public class Rest : BasicSoundComponentBase +/// length (ex. "quarter" note) 長さ(音楽的な、「四分」音符、「全」休符のような長さを表す。) +/// is note/rest dotted. 付点かを表す論理型 +public class Rest(LengthType length, bool isDotted = false) : BasicSoundComponentBase(length, isDotted) { - /// - /// constructor コンストラクタ - /// - /// length (ex. "quarter" note) 長さ(音楽的な、「四分」音符、「全」休符のような長さを表す。) - /// is note/rest dotted. 付点かを表す論理型 - public Rest(LengthType length, bool isDotted = false) - : base(length, isDotted) { } + public override Rest Clone() + { + return new(Length, IsDotted); + } public override short[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) { diff --git a/src/SoundMaker/Sounds/Score/Tie.cs b/src/SoundMaker/Sounds/Score/Tie.cs index 9d3d7e4..3a21c3f 100644 --- a/src/SoundMaker/Sounds/Score/Tie.cs +++ b/src/SoundMaker/Sounds/Score/Tie.cs @@ -67,4 +67,22 @@ public int GetWaveArrayLength(SoundFormat format, int tempo) } return length; } + + /// + /// Creates a clone of the tie.
+ /// タイのクローンを作成するメソッド。 + ///
+ /// A new instance of the tie with the same properties.
+ /// 同じプロパティを持つタイの新しいインスタンス + ///
+ public Tie Clone() + { + var newTie = new Tie(BaseNote.Clone(), AdditionalNotes.Select(note => note.Clone()).ToArray()); + return newTie; + } + + ISoundComponent ISoundComponent.Clone() + { + return Clone(); + } } diff --git a/src/SoundMaker/Sounds/Score/Tuplet.cs b/src/SoundMaker/Sounds/Score/Tuplet.cs index d7e45b5..18f2a68 100644 --- a/src/SoundMaker/Sounds/Score/Tuplet.cs +++ b/src/SoundMaker/Sounds/Score/Tuplet.cs @@ -97,4 +97,22 @@ private int GetLengthPerOneComponent() } return count; } + + /// + /// Creates a clone of the tuplet.
+ /// 連符のクローンを作成するメソッド。 + ///
+ /// A new instance of the tuplet with the same properties.
+ /// 同じプロパティを持つ連符の新しいインスタンス + ///
+ public Tuplet Clone() + { + var cloned = new Tuplet(TupletComponents.Select(component => component.Clone()).ToArray(), Length, IsDotted); + return cloned; + } + + ISoundComponent ISoundComponent.Clone() + { + return Clone(); + } } diff --git a/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs b/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs index 0dbbd8b..78ad536 100644 --- a/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs +++ b/src/SoundMaker/Sounds/SoundChannels/SoundChannelBase.cs @@ -47,7 +47,7 @@ public SoundChannelBase(int tempo, SoundFormat format, PanType panType) Format = format; if (tempo <= 0) { - throw new ArgumentOutOfRangeException("'tempo' must be non-negative and greater than 0."); + throw new ArgumentOutOfRangeException(nameof(tempo), "'tempo' must be non-negative and greater than 0."); } Tempo = tempo; } @@ -55,7 +55,7 @@ public SoundChannelBase(int tempo, SoundFormat format, PanType panType) /// /// サウンドコンポーネントのリスト /// - protected List SoundComponents { get; private set; } = new List(); + protected List SoundComponents { get; private set; } = []; public SoundFormat Format { get; } @@ -100,7 +100,7 @@ public void RemoveAt(int index) { if (SoundComponents.Count <= index || index < 0) { - throw new ArgumentOutOfRangeException("index is less than 0 or index is equal to or greater than ComponentCount."); + throw new ArgumentOutOfRangeException(nameof(index), "index is less than 0 or index is equal to or greater than ComponentCount."); } var component = SoundComponents[index]; WaveArrayLength -= component.GetWaveArrayLength(Format, Tempo); diff --git a/src/SoundMaker/Sounds/Track.cs b/src/SoundMaker/Sounds/Track.cs new file mode 100644 index 0000000..7aed704 --- /dev/null +++ b/src/SoundMaker/Sounds/Track.cs @@ -0,0 +1,260 @@ +using SoundMaker.Sounds.Score; +using SoundMaker.Sounds.WaveTypes; + +namespace SoundMaker.Sounds; + +/// +/// Represents a track with a specific wave type.
+/// 特定の波形タイプを持つトラックを表すクラス。 +///
+public class Track +{ + private List _soundComponents = []; + + private readonly SoundFormat _format; + + private readonly int _tempo; + + /// + /// Initializes a new instance of the Track class.
+ /// Track クラスの新しいインスタンスを初期化するコンストラクタ。 + ///
+ /// The wave type.
波形タイプ。 + /// The sound format.
サウンドフォーマット。 + /// The tempo.
テンポ。 + /// The start time in milliseconds.
開始時間(ミリ秒)。 + internal Track(WaveTypeBase waveType, SoundFormat format, int tempo, int startMilliSecond) + { + WaveType = waveType; + _format = format; + _tempo = tempo; + StartMilliSecond = startMilliSecond; + } + + + /// + /// Sound components
+ /// サウンドコンポーネント + ///
+ public IReadOnlyList SoundComponents => _soundComponents; + + private double _pan = 0; + /// + /// 左右の音量バランスを取得または設定するプロパティ。
+ /// -1.0が左、1.0が右側。
+ /// Gets or sets the left-right audio balance.
+ /// Takes values from -1.0 (left) to 1.0 (right). + ///
+ public double Pan + { + get => _pan; + set + { + value = value > 1.0 ? 1.0 : value; + value = value < -1.0 ? -1.0 : value; + + _pan = value; + } + } + + internal int EndIndex { get; private set; } + internal int StartIndex { get; private set; } + private int _startMilliSecond; + /// + /// Gets or sets the start time in milliseconds.
+ /// 開始時間(ミリ秒)を取得または設定するプロパティ。 + ///
+ internal int StartMilliSecond + { + get => _startMilliSecond; + set + { + // 負の数は許可しない + if (value < 0) + { + value = 0; + } + + _startMilliSecond = value; + + // 開始ミリ秒が変わると開始時、終了時のインデクスも変わるので、再計算する + var samplingFrequencyMS = (int)_format.SamplingFrequency / 1000.0; + StartIndex = (int)(StartMilliSecond * samplingFrequencyMS); + if (WaveArrayLength == 0) + { + EndIndex = StartIndex; + } + else + { + EndIndex = StartIndex + WaveArrayLength - 1; + } + } + } + + private int _waveArrayLength; + /// + /// Gets the length of the wave array.
+ /// 波形配列の長さを取得するプロパティ。 + ///
+ public int WaveArrayLength + { + get => _waveArrayLength; + set + { + _waveArrayLength = value; + + // 配列の長さが変わると終了時インデクスが変わるので、再計算する + if (WaveArrayLength == 0) + { + EndIndex = StartIndex; + } + else + { + EndIndex = StartIndex + WaveArrayLength - 1; + } + } + } + + /// + /// Count of sound components.
+ /// サウンドコンポーネントの数 + ///
+ public int Count => _soundComponents.Count; + + /// + /// Gets or sets the wave type.
+ /// 波形タイプを取得または設定するプロパティ。 + ///
+ public WaveTypeBase WaveType { get; set; } + + /// + /// Generates a wave based on the sound components.
+ /// サウンドコンポーネントに基づいて波形を生成するメソッド。 + ///
+ /// + /// An array of shorts representing the generated wave.
+ /// 生成された波形を表すショート型の配列。 + ///
+ public short[] GenerateWave() + { + var result = new List(); + foreach (var soundComponent in _soundComponents) + { + var wave = soundComponent.GenerateWave(_format, _tempo, WaveType); + result.AddRange(wave); + } + + return [.. result]; + } + + + /// + /// Adds a sound component to the track.
+ /// トラックにサウンドコンポーネントを追加するメソッド。 + ///
+ /// The sound component to add.
追加するサウンドコンポーネント。 + public void Add(ISoundComponent component) + { + WaveArrayLength += component.GetWaveArrayLength(_format, _tempo); + _soundComponents.Add(component); + } + + /// + /// Inserts a sound component at the specified index.
+ /// 指定されたインデックスにサウンドコンポーネントを挿入するメソッド。 + ///
+ /// The zero-based index at which the component should be inserted.
+ /// コンポーネントを挿入するゼロベースのインデックス。 + /// The sound component to insert.
+ /// 挿入するサウンドコンポーネント。 + /// Thrown when the index is out of range.
+ /// インデックスが範囲外の場合にスローされる例外。
+ + public void Insert(int index, ISoundComponent component) + { + if (IsOutOfRange(index)) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + _soundComponents.Insert(index, component); + WaveArrayLength += component.GetWaveArrayLength(_format, _tempo); + } + + /// + /// Removes a sound component at the specified index.
+ /// 指定されたインデックスのサウンドコンポーネントを削除するメソッド。 + ///
+ /// The index of the sound component to remove.
削除するサウンドコンポーネントのインデックス。 + /// Thrown when the index is out of range.
インデックスが範囲外の場合にスローされる例外。
+ public void RemoveAt(int index) + { + if (IsOutOfRange(index)) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + var targetComponent = _soundComponents[index]; + _soundComponents.Remove(targetComponent); + WaveArrayLength -= targetComponent.GetWaveArrayLength(_format, _tempo); + } + + /// + /// Removes a specified sound component.
+ /// 指定されたサウンドコンポーネントを削除するメソッド。 + ///
+ /// The sound component to remove.
削除するサウンドコンポーネント。 + /// True if the component was removed; otherwise, false.
コンポーネントが削除された場合は true、それ以外の場合は false。
+ public bool Remove(ISoundComponent component) + { + var ok = _soundComponents.Remove(component); + if (ok) + { + WaveArrayLength -= component.GetWaveArrayLength(_format, _tempo); + return true; + } + + return false; + } + + /// + /// Clears all sound components from the track.
+ /// トラックからすべてのサウンドコンポーネントをクリアするメソッド。 + ///
+ public void Clear() + { + WaveArrayLength = 0; + _soundComponents.Clear(); + } + + /// + /// Imports a collection of sound components into the track.
+ /// トラックにサウンドコンポーネントのコレクションをインポートするメソッド。 + ///
+ /// The collection of sound components to import.
インポートするサウンドコンポーネントのコレクション。 + public void Import(IEnumerable components) + { + _soundComponents = new List(components); + WaveArrayLength = components.Sum(component => component.GetWaveArrayLength(_format, _tempo)); + } + + /// + /// Creates a clone of the track.
+ /// トラックのクローンを作成するメソッド。 + ///
+ /// A new instance of the track with the same properties.
同じプロパティを持つトラックの新しいインスタンス。
+ internal Track Clone() + { + var copy = new Track(WaveType.Clone(), _format, _tempo, StartMilliSecond) + { + WaveArrayLength = WaveArrayLength, + _soundComponents = _soundComponents.Select(component => component.Clone()).ToList() + }; + return copy; + } + + private bool IsOutOfRange(int index) + { + return index < 0 || index >= _soundComponents.Count; + } +} diff --git a/src/SoundMaker/Sounds/TrackBaseSound.cs b/src/SoundMaker/Sounds/TrackBaseSound.cs new file mode 100644 index 0000000..c371d06 --- /dev/null +++ b/src/SoundMaker/Sounds/TrackBaseSound.cs @@ -0,0 +1,336 @@ +using SoundMaker.Sounds.WaveTypes; + +namespace SoundMaker.Sounds; + +/// +/// Initializes a new instance of the TrackBaseSound class with the specified format and tempo.
+/// 指定されたフォーマットとテンポでTrackBaseSoundクラスの新しいインスタンスを初期化するメソッド。 +///
+/// The sound format to be used.
+/// 使用するサウンドフォーマット。 +/// +/// The tempo of the track.
+/// トラックのテンポ。 +/// +public class TrackBaseSound(SoundFormat format, int tempo) +{ + /// + /// Gets the tempo value.
+ /// テンポの値を取得するプロパティ。 + ///
+ public int Tempo { get; } = tempo; + + /// + /// Gets the sound format.
+ /// サウンドフォーマットを取得するプロパティ。 + ///
+ public SoundFormat Format { get; } = format; + + /// + /// トラックを管理する辞書
+ /// 開始時間(ミリ秒)とトラック(複数)のペア + ///
+ private Dictionary> _tracksTimeMap = []; + + /// + /// Creates a new track with the specified wave type and start time.
+ /// 指定された波の種類と開始時間で新しいトラックを作成するメソッド。 + ///
+ /// The start time in milliseconds.
開始時間(ミリ秒)。 + /// The type of wave.
波の種類。 + /// A new instance of the track.
新しいトラックのインスタンス。
+ public Track CreateTrack(int startMilliSecond, WaveTypeBase waveType) + { + var track = new Track(waveType, Format, Tempo, startMilliSecond); + InsertTrack(startMilliSecond, track); + return track; + } + + /// + /// Removes all tracks at the specified start time.
+ /// 指定された開始時間のすべてのトラックを削除するメソッド。 + ///
+ /// The start time in milliseconds.
開始時間(ミリ秒)。 + /// True if tracks were removed; otherwise, false.
トラックが削除された場合は true、それ以外の場合は false。
+ public bool RemoveTracksAt(int startMilliSecond) + { + return _tracksTimeMap.Remove(startMilliSecond); + } + + /// + /// Removes the specified track.
+ /// 指定されたトラックを削除するメソッド。 + ///
+ /// The track to remove.
削除するトラック。 + /// True if the track was removed; otherwise, false.
トラックが削除された場合は true、それ以外の場合は false。
+ public bool RemoveTrack(Track track) + { + if (_tracksTimeMap.TryGetValue(track.StartMilliSecond, out var tracks)) + { + var ok = tracks.Remove(track); + if (!ok) + { + return false; + } + + if (tracks.Count == 0) + { + return _tracksTimeMap.Remove(track.StartMilliSecond); + } + + return true; + } + + return false; + } + + /// + /// Gets the list of tracks at the specified start time.
+ /// 指定された開始時間のトラックのリストを取得するメソッド。 + ///
+ /// The start time in milliseconds.
開始時間(ミリ秒)。 + /// + /// A list of tracks.
+ /// トラックのリスト。 + /// If the operation fails, an empty list is returned.
+ /// 失敗時は空リスト。 + ///
+ + public List GetTracks(int startMilliSecond) + { + if (_tracksTimeMap.TryGetValue(startMilliSecond, out var tracks)) + { + return tracks; + } + else + { + return []; + } + } + + /// + /// Retrieves all tracks.
+ /// すべてのトラックを取得するメソッド。 + ///
+ /// An enumerable collection of tracks.
+ /// トラックの列挙可能なコレクションを返します。 + ///
+ public IEnumerable GetAllTracks() + { + return _tracksTimeMap.SelectMany(pair => pair.Value); + } + + /// + /// Tries to get the list of tracks at the specified start time.
+ /// 指定された開始時間のトラックのリストを取得しようとするメソッド。 + ///
+ /// The start time in milliseconds.
開始時間(ミリ秒)。 + /// The list of tracks.
トラックのリスト。 + /// True if tracks were found; otherwise, false.
トラックが見つかった場合は true、それ以外の場合は false。
+ public bool TryGetTracks(int startMilliSecond, out List tracks) + { + if (_tracksTimeMap.TryGetValue(startMilliSecond, out var foundTracks)) + { + tracks = foundTracks; + return true; + } + else + { + tracks = []; + return false; + } + } + + /// + /// Inserts a track at the specified start time.
+ /// 指定された開始時間にトラックを挿入するメソッド。 + ///
+ /// The start time in milliseconds.
開始時間(ミリ秒)。 + /// The track to insert.
挿入するトラック。 + private void InsertTrack(int startMilliSecond, Track track) + { + if (_tracksTimeMap.TryGetValue(startMilliSecond, out var tracks)) + { + tracks.Add(track); + } + else + { + _tracksTimeMap[startMilliSecond] = [track]; + } + } + + /// + /// Moves a track to a new start time.
+ /// トラックを新しい開始時間に移動するメソッド。 + ///
+ /// The track to move.
移動するトラック。 + /// The new start time in milliseconds.
新しい開始時間(ミリ秒)。 + /// True if the track was moved; otherwise, false.
トラックが移動された場合は true、それ以外の場合は false。
+ public bool MoveTrack(Track track, int newStartMilliSecond) + { + if (RemoveTrack(track)) + { + InsertTrack(newStartMilliSecond, track); + track.StartMilliSecond = newStartMilliSecond; + return true; + } + return false; + } + + /// + /// Creates a copy of the specified track at a new start time.
+ /// 指定されたトラックのコピーを新しい開始時間に作成するメソッド。 + ///
+ /// The track to copy.
コピーするトラック。 + /// The new start time in milliseconds.
新しい開始時間(ミリ秒)。 + /// A new instance of the copied track.
コピーされたトラックの新しいインスタンス。
+ public Track CopyTrack(Track sourceTrack, int newStartMilliSecond) + { + var newTrack = sourceTrack.Clone(); + newTrack.StartMilliSecond = newStartMilliSecond; + InsertTrack(newStartMilliSecond, newTrack); + return newTrack; + } + + /// + /// Clears all tracks.
+ /// すべてのトラックをクリアするメソッド。 + ///
+ public void Clear() + { + _tracksTimeMap.Clear(); + } + + /// + /// Generates a monaural wave from the tracks.
+ /// トラックからモノラル波を生成するメソッド。 + ///
+ /// A monaural wave.
モノラル波。
+ public MonauralWave GenerateMonauralWave() + { + if (_tracksTimeMap.Count == 0) + { + return new([]); + } + + // 最大の終了時インデクスを取得する + var maxEndIndex = _tracksTimeMap + .SelectMany(pair => pair.Value) + .Max(track => track.EndIndex); + + var wave = new double[maxEndIndex + 1]; + + foreach (var (_, tracks) in _tracksTimeMap) + { + foreach (var track in tracks) + { + if (track.Count == 0) + { + continue; + } + + var trackWave = track.GenerateWave(); + for (int i = track.StartIndex; i <= track.EndIndex; i++) + { + wave[i] += trackWave[i - track.StartIndex]; + } + } + } + + var normalizedRight = NormalizeAndClamp(wave); + return new(normalizedRight); + } + + /// + /// Generates a stereo wave from the tracks.
+ /// トラックからステレオ波を生成するメソッド。 + ///
+ /// A stereo wave.
ステレオ波。
+ public StereoWave GenerateStereoWave() + { + if (_tracksTimeMap.Count == 0) + { + return new([], []); + } + + // 最大の終了時インデクスを取得する + var maxEndIndex = _tracksTimeMap + .SelectMany(pair => pair.Value) + .Max(track => track.EndIndex); + + var right = new double[maxEndIndex + 1]; + var left = new double[maxEndIndex + 1]; + + foreach (var (_, tracks) in _tracksTimeMap) + { + foreach (var track in tracks) + { + if (track.Count == 0) + { + continue; + } + + var trackWave = track.GenerateWave(); + var pan = (track.Pan + 1) / 2.0f; + for (int i = track.StartIndex; i <= track.EndIndex; i++) + { + left[i] += trackWave[i - track.StartIndex] * pan; + right[i] += trackWave[i - track.StartIndex] * (1 - pan); + } + } + } + + var normalizedRight = NormalizeAndClamp(right); + var normalizedLeft = NormalizeAndClamp(left); + return new(normalizedRight, normalizedLeft); + } + + /// + /// Normalizes and clamps the wave data.
+ /// 波形データを正規化してクランプするメソッド。 + ///
+ /// The wave data.
波形データ。 + /// The normalized and clamped wave data.
正規化およびクランプされた波形データ。
+ private static short[] NormalizeAndClamp(double[] wave) + { + const int MaxValue = short.MaxValue; + const int MinValue = short.MinValue; + + var maxAmplitude = wave.Max(Math.Abs); + var scaleFactor = 1.0; + + if (maxAmplitude > MaxValue) + { + scaleFactor = MaxValue / maxAmplitude; + } + + return wave.Select(sample => + { + var scaledSample = sample * scaleFactor; + return (short)Math.Clamp(scaledSample, MinValue, MaxValue); + }).ToArray(); + } + + /// + /// Imports tracks into the internal map based on their start times.
+ /// トラックを開始時間に基づいて内部のマップにインポートするメソッド。 + ///
+ /// The tracks to import.
インポートするトラック。 + public void Import(IEnumerable from) + { + var map = new Dictionary>(); + foreach (var track in from) + { + if (map.TryGetValue(track.StartMilliSecond, out var tracks)) + { + tracks.Add(track); + } + else + { + map.Add(track.StartMilliSecond, [track]); + } + } + + _tracksTimeMap = map; + } +} diff --git a/src/SoundMaker/Sounds/WaveTypes/LowBitNoiseWave.cs b/src/SoundMaker/Sounds/WaveTypes/LowBitNoiseWave.cs index c4195d4..ebe8e75 100644 --- a/src/SoundMaker/Sounds/WaveTypes/LowBitNoiseWave.cs +++ b/src/SoundMaker/Sounds/WaveTypes/LowBitNoiseWave.cs @@ -29,4 +29,9 @@ public override short[] GenerateWave(SoundFormat format, int length, int volume, } return result.ToArray(); } + + internal override WaveTypeBase Clone() + { + return new LowBitNoiseWave(); + } } diff --git a/src/SoundMaker/Sounds/WaveTypes/PseudoTriangleWave.cs b/src/SoundMaker/Sounds/WaveTypes/PseudoTriangleWave.cs index 76956b0..c413a97 100644 --- a/src/SoundMaker/Sounds/WaveTypes/PseudoTriangleWave.cs +++ b/src/SoundMaker/Sounds/WaveTypes/PseudoTriangleWave.cs @@ -38,6 +38,11 @@ public override short[] GenerateWave(SoundFormat format, int length, int volume, return result.ToArray(); } + internal override WaveTypeBase Clone() + { + return new PseudoTriangleWave(); + } + private List GenerateUnitWave(SoundFormat format, int volume, double hertz) { var repeatNumber = (int)((int)format.SamplingFrequency / hertz); diff --git a/src/SoundMaker/Sounds/WaveTypes/SquareWave.cs b/src/SoundMaker/Sounds/WaveTypes/SquareWave.cs index 091368c..c576ee3 100644 --- a/src/SoundMaker/Sounds/WaveTypes/SquareWave.cs +++ b/src/SoundMaker/Sounds/WaveTypes/SquareWave.cs @@ -42,6 +42,11 @@ public override short[] GenerateWave(SoundFormat format, int length, int volume, return result.ToArray(); } + internal override WaveTypeBase Clone() + { + return new SquareWave(SquareWaveRatio); + } + private List GenerateUnitWave(SoundFormat format, int volume, double hertz) { var ratioIndex = (int)SquareWaveRatio; diff --git a/src/SoundMaker/Sounds/WaveTypes/TriangleWave.cs b/src/SoundMaker/Sounds/WaveTypes/TriangleWave.cs index 226be12..cded92f 100644 --- a/src/SoundMaker/Sounds/WaveTypes/TriangleWave.cs +++ b/src/SoundMaker/Sounds/WaveTypes/TriangleWave.cs @@ -20,6 +20,11 @@ public override short[] GenerateWave(SoundFormat format, int length, int volume, return result.ToArray(); } + internal override WaveTypeBase Clone() + { + return new TriangleWave(); + } + private List GenerateUnitWave(SoundFormat format, int volume, double hertz) { var repeatNumber = (int)((int)format.SamplingFrequency / hertz); diff --git a/src/SoundMaker/Sounds/WaveTypes/WaveTypeBase.cs b/src/SoundMaker/Sounds/WaveTypes/WaveTypeBase.cs index a9304df..a7323d3 100644 --- a/src/SoundMaker/Sounds/WaveTypes/WaveTypeBase.cs +++ b/src/SoundMaker/Sounds/WaveTypes/WaveTypeBase.cs @@ -17,6 +17,8 @@ public abstract class WaveTypeBase /// Volume must be below than 100 and more than 0. public abstract short[] GenerateWave(SoundFormat format, int length, int volume, double hertz); + internal abstract WaveTypeBase Clone(); + protected void CheckGenerateWaveArgs(int length, int volume, double hertz) { if (length < 0) diff --git a/test/IntegrationTests/Sounds/SoundChannels/TestSquareSoundChannel.cs b/test/IntegrationTests/Sounds/SoundChannels/TestSquareSoundChannel.cs deleted file mode 100644 index 772e6cd..0000000 --- a/test/IntegrationTests/Sounds/SoundChannels/TestSquareSoundChannel.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace SoundMakerTests.IntegrationTests.Sounds.SoundChannels; -internal class TestSquareSoundChannel -{ -} diff --git a/test/SoundMakerTests.csproj b/test/SoundMakerTests.csproj index 9fd1171..f80eebb 100644 --- a/test/SoundMakerTests.csproj +++ b/test/SoundMakerTests.csproj @@ -8,12 +8,6 @@ false - - - - - - diff --git a/test/UnitTests/Sounds/Score/TestTuplet.cs b/test/UnitTests/Sounds/Score/TestTuplet.cs index 7a85042..8d3c771 100644 --- a/test/UnitTests/Sounds/Score/TestTuplet.cs +++ b/test/UnitTests/Sounds/Score/TestTuplet.cs @@ -7,6 +7,11 @@ public class TestTuplet { private class SoundComponent : ISoundComponent { + public ISoundComponent Clone() + { + throw new NotImplementedException(); + } + public short[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) { return new short[0]; diff --git a/test/UnitTests/Sounds/SoundChannels/TestSoundChannelBase.cs b/test/UnitTests/Sounds/SoundChannels/TestSoundChannelBase.cs index af1c42f..9b41b69 100644 --- a/test/UnitTests/Sounds/SoundChannels/TestSoundChannelBase.cs +++ b/test/UnitTests/Sounds/SoundChannels/TestSoundChannelBase.cs @@ -21,6 +21,11 @@ public override short[] GenerateWave() private class SoundComponent : ISoundComponent { + public ISoundComponent Clone() + { + throw new NotImplementedException(); + } + public short[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) { return new short[0]; @@ -93,43 +98,6 @@ public void ClearTest() Assert.Equal(0, soundChannel.ComponentCount); } - /* - [Fact(DisplayName = "楽譜のパーサをもとに、サウンドコンポーネントが追加されるかのテスト")] - public void ImportScoreTest() - { - SoundChannelBase soundChannel = GetSoundChannel(); - - IScoreParser scoreParser = new ScoreParser(); - soundChannel.ImportScore(scoreParser); - (Scale scale, int scaleNum)[] scales = { (Scale.A, 2), (Scale.A, 3), (Scale.A, 4) }; - - bool isValid = true; - for (int i = 0; i < scales.Length && i < soundChannel.ComponentCount; i++) - { - if (soundChannel[i] is not Note note || - note.Scale != scales[i].scale || - note.ScaleNumber != scales[i].scaleNum) - { - isValid = false; - break; - } - } - Assert.True(isValid); - } - - private class ScoreParser : IScoreParser - { - public IEnumerable Parse() - { - return new List() - { - new Note(Scale.A, 2, LengthType.Whole), - new Note(Scale.A, 3, LengthType.Whole), - new Note(Scale.A, 4, LengthType.Whole), - }; - } - }*/ - private SoundChannelBase GetSoundChannel() { var tempo = 100; diff --git a/test/UnitTests/Sounds/TrackBaseSoundTest.cs b/test/UnitTests/Sounds/TrackBaseSoundTest.cs new file mode 100644 index 0000000..d5f6bda --- /dev/null +++ b/test/UnitTests/Sounds/TrackBaseSoundTest.cs @@ -0,0 +1,201 @@ +using SoundMaker; +using SoundMaker.Sounds; +using SoundMaker.Sounds.SoundChannels; +using SoundMaker.Sounds.WaveTypes; + +namespace SoundMakerTests.UnitTests.Sounds; +public class TrackBaseSoundTest +{ + [Fact(DisplayName = "すべてのトラックを取得できるかをテストする。")] + public void GetAllTracks() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var track1 = sound.CreateTrack(0, wave); + var track2 = sound.CreateTrack(1000, wave); + + var actual = sound.GetAllTracks().ToArray(); + + Assert.Equal(2, actual.Length); + Assert.Equal(track1, actual[0]); + Assert.True(track2.Equals(actual[1])); + } + + [Fact(DisplayName = "指定したミリ秒から開始するトラックをすべて削除できるか")] + public void RemoveTracksAt() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + // 0ミリ秒開始のトラックを削除する対象とする + var targetStartMS = 0; + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + _ = sound.CreateTrack(targetStartMS, wave); + _ = sound.CreateTrack(targetStartMS, wave); + var track1 = sound.CreateTrack(1000, wave); + + sound.RemoveTracksAt(targetStartMS); + + Assert.False(sound.TryGetTracks(targetStartMS, out _)); + Assert.Contains(track1, sound.GetAllTracks()); + } + + [Fact(DisplayName = "トラックの削除を正しく行えるか")] + public void RemoveTrack() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var track1 = sound.CreateTrack(0, wave); + var track2 = sound.CreateTrack(1000, wave); + + var maybeOk = sound.RemoveTrack(track1); + + var tracks = sound.GetAllTracks(); + + Assert.True(maybeOk); + Assert.DoesNotContain(track1, tracks); + Assert.Contains(track2, tracks); + + var maybeFail = sound.RemoveTrack(new Track(new TriangleWave(), format, 100, 10000)); + Assert.False(maybeFail); + } + + [Fact(DisplayName = "指定したミリ秒から開始するトラックをすべて取得できるか")] + public void GetTracks() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var track1 = sound.CreateTrack(0, wave); + + var tracks = sound.GetTracks(track1.StartMilliSecond); + Assert.Contains(track1, tracks); + } + + [Fact(DisplayName = "存在しない開始位置でトラックを取得しようとした場合に空リストが返るか")] + public void GetTracks_Empty() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + + var maybeEmpty = sound.GetTracks(0); + + Assert.Empty(maybeEmpty); + } + + [Fact(DisplayName = "トラック取得に成功した場合、失敗した場合")] + public void TryGetTracks() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var track1 = sound.CreateTrack(0, wave); + + Assert.True(sound.TryGetTracks(track1.StartMilliSecond, out var tracks)); + Assert.Contains(track1, tracks); + + Assert.False(sound.TryGetTracks(1000, out _)); + } + + [Fact(DisplayName = "トラックを指定した時間で挿入できる。")] + public void Insert() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var track1 = sound.CreateTrack(0, wave); + var track2 = sound.CreateTrack(1000, wave); + + Assert.Equal(track1, sound.GetTracks(track1.StartMilliSecond)[0]); + Assert.Equal(track2, sound.GetTracks(track2.StartMilliSecond)[0]); + } + + [Fact(DisplayName = "トラックの移動を行えるか")] + public void MoveTrack() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var oldStartMilliSecond = 0; + var track1 = sound.CreateTrack(oldStartMilliSecond, wave); + + var newStartMilliSecond = 1000; + sound.MoveTrack(track1, newStartMilliSecond); + + Assert.Equal(track1, sound.GetTracks(newStartMilliSecond)[0]); + Assert.Empty(sound.GetTracks(oldStartMilliSecond)); + } + + [Fact(DisplayName = "トラックのコピーを行えるか")] + public void CopyTrack() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + var oldStartMilliSecond = 0; + var track1 = sound.CreateTrack(oldStartMilliSecond, wave); + + var newStartMilliSecond = 1000; + sound.CopyTrack(track1, newStartMilliSecond); + + Assert.Equal(track1, sound.GetTracks(oldStartMilliSecond)[0]); + + var copied = sound.GetTracks(newStartMilliSecond); + Assert.NotEmpty(copied); + } + + [Fact(DisplayName = "空にできるか")] + public void Clear() + { + var format = FormatBuilder.Create() + .WithFrequency(44100) + .WithBitDepth(8) + .WithChannelCount(1) + .ToSoundFormat(); + var sound = new TrackBaseSound(format, 100); + var wave = new SquareWave(SquareWaveRatio.Point25); + _ = sound.CreateTrack(0, wave); + _ = sound.CreateTrack(1000, wave); + + sound.Clear(); + + Assert.Empty(sound.GetAllTracks()); + } +} diff --git a/test/UnitTests/Sounds/TrackTest.cs b/test/UnitTests/Sounds/TrackTest.cs new file mode 100644 index 0000000..a195ac8 --- /dev/null +++ b/test/UnitTests/Sounds/TrackTest.cs @@ -0,0 +1,178 @@ +using SoundMaker; +using SoundMaker.Sounds; +using SoundMaker.Sounds.Score; +using SoundMaker.Sounds.WaveTypes; + +namespace SoundMakerTests.UnitTests.Sounds; + +file class SoundComponentDouble : ISoundComponent +{ + public static readonly int DefinedGenerateWaveLength = 1; + + public ISoundComponent Clone() + { + return new SoundComponentDouble(); + } + + public short[] GenerateWave(SoundFormat format, int tempo, int length, WaveTypeBase waveType) + { + return []; + } + + public short[] GenerateWave(SoundFormat format, int tempo, WaveTypeBase waveType) + { + return [0]; + } + + public int GetWaveArrayLength(SoundFormat format, int tempo) + { + return DefinedGenerateWaveLength; + } +} + +public class TrackTest +{ + private static readonly int _samplingFrequency = 48000; + + [Fact(DisplayName = "サウンドコンポーネントをインポートできるか")] + public void Import() + { + var track = CreateTrack(); + + var additionalComponent0 = new SoundComponentDouble(); + var additionalComponent1 = new SoundComponentDouble(); + + track.Import([additionalComponent0, additionalComponent1]); + + Assert.Equal(additionalComponent0, track.SoundComponents[0]); + Assert.Equal(additionalComponent1, track.SoundComponents[1]); + + AssertEndIndex(track); + } + + [Fact(DisplayName = "生成した波形の長さがWaveArrayLengthと同じ長さか")] + public void GenerateWave() + { + var track = CreateTrack(); + var dummyComponent = new SoundComponentDouble(); + track.Import([dummyComponent, dummyComponent, dummyComponent]); + + var wave = track.GenerateWave(); + + Assert.Equal(track.WaveArrayLength, wave.Length); + } + + [Fact(DisplayName = "コンポーネントを追加できるか")] + public void Add() + { + var track = CreateTrack(); + var additionalComponent = new SoundComponentDouble(); + track.Add(additionalComponent); + + Assert.Equal(additionalComponent, track.SoundComponents[0]); + AssertEndIndex(track); + } + + [Fact(DisplayName = "コンポーネントを挿入できるか。例外が正しく投げられるか")] + public void Insert() + { + var track = CreateTrack(); + + var dummyComponent = new SoundComponentDouble(); + var insertTargetComponent = new SoundComponentDouble(); + // トラックにダミーのコンポーネントを追加 + track.Import([dummyComponent, dummyComponent]); + var insertMaybeOkIndex = 1; + track.Insert(1, insertTargetComponent); + + Assert.Equal(insertTargetComponent, track.SoundComponents[insertMaybeOkIndex]); + + Assert.Throws(() => track.Insert(-1, dummyComponent)); + Assert.Throws(() => track.Insert(4, dummyComponent)); + AssertEndIndex(track); + } + + [Fact(DisplayName = "コンポーネントをインデクスで削除できるか。例外が正しく投げられるか")] + public void RemoveAt() + { + var track = CreateTrack(); + + var dummyComponent = new SoundComponentDouble(); + + track.Add(dummyComponent); + + track.RemoveAt(0); + + Assert.Empty(track.SoundComponents); + + track.Add(dummyComponent); + Assert.Throws(() => track.RemoveAt(-1)); + Assert.Throws(() => track.RemoveAt(1)); + AssertEndIndex(track); + } + + [Fact(DisplayName = "サウンドコンポーネントを削除できるか")] + public void Remove() + { + var track = CreateTrack(); + var dummyComponent = new SoundComponentDouble(); + track.Add(dummyComponent); + track.Remove(dummyComponent); + + Assert.Empty(track.SoundComponents); + AssertEndIndex(track); + } + + [Fact(DisplayName = "サウンドコンポーネントをクリアできるか")] + public void Clear() + { + var track = CreateTrack(); + var dummyComponent = new SoundComponentDouble(); + + track.Import([dummyComponent, dummyComponent, dummyComponent]); + + track.Clear(); + + Assert.Empty(track.SoundComponents); + AssertEndIndex(track); + } + + [Fact(DisplayName = "開始位置を変更した際にインデクス関連のプロパティが正しく変更されるか")] + public void ChangeStartMilliSecond() + { + var track = CreateTrack(); + var oldStartIndex = track.StartIndex; + var oldEndIndex = track.EndIndex; + var diffIndex = _samplingFrequency; + // 1000ミリ秒減らす + track.StartMilliSecond -= 1000; + + var expectedEndIndex = oldEndIndex - diffIndex; + Assert.Equal(expectedEndIndex, track.EndIndex); + } + + private Track CreateTrack() + { + var format = FormatBuilder.Create() + .WithFrequency(_samplingFrequency) + .WithBitDepth(16) + .WithChannelCount(2) + .ToSoundFormat(); + return new Track(new TriangleWave(), format, 100, 1000); + } + + private void AssertEndIndex(Track track) + { + int expectedEndIndex; + if (track.Count == 0) + { + expectedEndIndex = track.StartIndex; + } + else + { + expectedEndIndex = track.StartIndex + track.Count * SoundComponentDouble.DefinedGenerateWaveLength - 1; + } + + Assert.Equal(expectedEndIndex, track.EndIndex); + } +} diff --git a/test/IntegrationTests/TestFormatBuilder.cs b/test/UnitTests/TestFormatBuilder.cs similarity index 96% rename from test/IntegrationTests/TestFormatBuilder.cs rename to test/UnitTests/TestFormatBuilder.cs index 4a2e0e6..c853d9b 100644 --- a/test/IntegrationTests/TestFormatBuilder.cs +++ b/test/UnitTests/TestFormatBuilder.cs @@ -1,6 +1,6 @@ using SoundMaker; -namespace SoundMakerTests.IntegrationTests; +namespace SoundMakerTests.UnitTests; public class TestFormatBuilder { [Fact()]