/** <summary>Get the singleton instance.</summary> */ static public Metronome GetInstance() { if (Instance == null) { Instance = new Metronome(); } return(Instance); }
static void Main(string[] args) { // TODO: Main runs in a different thread than the audio streams. Deal with thread safety. // if a future hihat closed sound is not muted, calculate its position in byte time, var hhc. // (number of cycles * cycle size + value of old byteInterval at start of present cycle + new byteInterval) // in the hihat open sound thread, determine how many bytes until hhc is reached. Metronome metronome = Metronome.GetInstance(); metronome.Tempo = 75f; //new Layer("[.25,.25@C4,.25@A2,.25@A3,.25@Bb2,1.75@Bb3]2,[.25@F2,.25@F3,.25@D2,.25@D3,.25@Eb2,1.75@Eb3](2)-.5,1/6@Eb3,1/6@D3,1/6@Db3,.5@C3,.5@Eb3,.5@D3,.5@Ab2,.5@G2,.5@Db3,1/6@C3,1/6@F#3,1/6@F3,1/6@E3,1/6@Bb3,1/6@A3,1/3@Ab3,1/3@Eb3,1/3@B2,1/3@Bb2,1/3@A2,1/3+3@Ab2,{$s}2/3", "C3", "", 0, .7f); //new Layer("[.5,.5+.75,.5,.75]4,.5,.25,.5,.25,.75,.25,.5,!1!.5,.5+.75,.5,.75!2!,.5,.75,.5,1,.25,{$s}2/3", WavFileStream.GetFileByName("Kick Drum V2")); //new Layer("[[1@0,1.5|.5]2,.25(2)]2,1@0,2,!1!1@0,.5,1.25!2.75!,.5,.75,1.5,.25(2),{$s}2/3", WavFileStream.GetFileByName("Snare Rim V3")); //new Layer("[.5(6)]7,{$s}2/3", WavFileStream.GetFileByName("HiHat Half Center V1")); //new Layer("1", "A6"); new Layer(".5@0,[.5(3),.5@47,.25,.5,1.25]2,!3!.75,1,.75,.5@47,.25,1.5,!4!.5@47,1.5,.5,.25,{$s}2/3,{$s}4/5", WavFileStream.GetFileByName("Snare Center V2")); new Layer("[.75,3.25]3,.75,1.25,.75,1.25,{$s}2/3,{$s}4/5", WavFileStream.GetFileByName("Kick Drum V3")); new Layer("3*4+.25@0,.25@28,3.5,{$s}2/3,{$s}4/5", WavFileStream.GetFileByName("FloorTom V2")); new Layer(".25@0,[1,.5,.25@20,.25@22,.25@20,.5@22,.5,.25,.5]2,!3!.75,.5,1/6,1/6@12(2),.5,.25,.5,.75+1,!4!.5,.75,.25,.5(2),.5,{$s}2/3,{$s}4/5", WavFileStream.GetFileByName("HiHat Half Center V2")); new Layer("1", "A7"); //new Layer("[.75@0,.75,.5,2]3,.75@0,.75,.5+.75,.75,.5", "A4"); //new Layer("1", "D3"); //metronome.SetSilentInterval(4, 4); //Metronome.Save("Slinky"); //todo: why does @0 on first of pitch layer get played? //var layer1 = new Layer("[1,2/3,1/3]4,{$s}2/3", "A4"); //new Layer("1", "A5"); //var layer2 = new Layer("1,2/3,1/3", WavFileStream.GetFileByName("Ride Center V3")); //new Layer("1,1,1/3@19", WavFileStream.GetFileByName("HiHat Pedal V2")); //Metronome.Load("metronome"); //var metronome = Metronome.GetInstance(); //metronome.SetRandomMute(50); //Thread.Sleep(500); metronome.Play(); //Console.ReadKey(); //metronome.Tempo = 20f; //metronome.Record("test"); //metronome.ExportAsWav(0, "test.wav"); Console.ReadKey(); metronome.Stop(); //metronome.ChangeTempo(50f); //Console.ReadKey(); //metronome.ChangeTempo(38f); //Console.ReadKey(); //metronome.Stop(); //metronome.Play(); //Console.ReadKey(); //metronome.Stop(); metronome.Dispose(); //Console.ReadKey(); }
/**<summary>Constructor</summary> * <param name="channel">Number of channels</param> * <param name="sampleRate">Samples per second</param> */ public PitchStream(int sampleRate = 16000, int channel = 2) { waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channel); // Default Frequency = BaseFrequency = 440.0; Pan = 0; BytesPerSec = waveFormat.AverageBytesPerSecond / 8; freqEnum = Frequencies.Values.GetEnumerator(); // set audible/silent interval if already exists if (Metronome.GetInstance().IsSilentInterval) { SetSilentInterval(Metronome.GetInstance().AudibleInterval, Metronome.GetInstance().SilentInterval); } }
/** <summary>Add to soloed group.</summary> */ public void ToggleSoloGroup() { if (IsSoloed) { // unsolo and close the solo group if this was the only member IsSoloed = false; if (Metronome.GetInstance().Layers.Where(x => x.IsSoloed == true).Count() == 0) { SoloGroupEngaged = false; } } else { // add this layer to solo group. all layers not in group will be muted. IsSoloed = true; SoloGroupEngaged = true; } }
protected bool IsRandomMuted() { bool result; if (!Metronome.GetInstance().IsRandomMute) { currentlyMuted = false; return(false); } // init countdown if (randomMuteCountdown == null && Metronome.GetInstance().RandomMuteSeconds > 0) { randomMuteCountdown = randomMuteCountdownTotal = Metronome.GetInstance().RandomMuteSeconds *BytesPerSec - initialOffset; } int rand = Metronome.GetRandomNum(); if (randomMuteCountdown == null) { result = rand < Metronome.GetInstance().RandomMutePercent; } else { // countdown if (randomMuteCountdown > 0) { randomMuteCountdown -= previousByteInterval; //previousByteInterval; } if (randomMuteCountdown < 0) { randomMuteCountdown = 0; } float factor = (float)(randomMuteCountdownTotal - randomMuteCountdown) / randomMuteCountdownTotal; result = rand < Metronome.GetInstance().RandomMutePercent *factor; } return(result); }
/**<summary>Reset state to default values.</summary>*/ public void Reset() { freqEnum.Reset(); //= Frequencies.Values.GetEnumerator(); BeatCollection.Enumerator = BeatCollection.GetEnumerator(); ByteInterval = 0; previousSample = 0; Gain = Volume; if (Metronome.GetInstance().IsSilentInterval) { SetSilentInterval(Metronome.GetInstance().AudibleInterval, Metronome.GetInstance().SilentInterval); } if (Metronome.GetInstance().IsRandomMute) { randomMuteCountdown = null; currentlyMuted = false; } if (Layer.Offset > 0) { SetOffset( BeatCell.ConvertFromBpm(Layer.Offset, this) ); } }
/** <summary>Layer constructor</summary> * <param name="baseSourceName">Name of the base sound source.</param> * <param name="beat">Beat code.</param> * <param name="offset">Amount of offset</param> * <param name="pan">Set the pan</param> * <param name="volume">Set the volume</param> */ public Layer(string beat, string baseSourceName = null, string offset = "", float pan = 0f, float volume = 1f) { if (baseSourceName == null) // auto generate a pitch if no source is specified { SetBaseSource(GetAutoPitch()); } else { SetBaseSource(baseSourceName); } if (offset != "") { SetOffset(offset); } Parse(beat); // parse the beat code into this layer Volume = volume; if (pan != 0f) { Pan = pan; } Metronome.GetInstance().AddLayer(this); }
/**<summary>Get a random pitch based on existing pitch layers</summary>*/ public string GetAutoPitch() { string note; byte octave; string[] noteNames = { "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#" }; ushort[] intervals = { 3, 4, 5, 7, 8, 9 }; do { // determine the octave octave = Metronome.GetRandomNum() > 49 ? (byte)5 : (byte)4; // 80% chance to make a sonorous interval with last pitch layer if (Metronome.GetRandomNum() < 80) { var last = Metronome.GetInstance().Layers.Last(x => IsPitch); int index = Array.IndexOf(noteNames, last.BaseSourceName.TakeWhile(x => !char.IsNumber(x))); index += intervals[Metronome.GetRandomNum() / (100 / 6)]; if (index > 11) { index -= 12; } note = noteNames[index]; } else { // randomly pick note note = noteNames[Metronome.GetRandomNum() / (100 / 12)]; } }while (Metronome.GetInstance().Layers.Where(x => x.IsPitch).Any(x => x.BaseSourceName == note + octave)); return(note + octave); }
/**<summary>Convert a quarter note time value into a byte count.</summary> * <param name="bpm">Number of quarter-notes.</param> * <param name="src">The audio source to get the bytes/second from.</param> */ static public double ConvertFromBpm(double bpm, IStreamProvider src) { double result = bpm * (60d / Metronome.GetInstance().Tempo) * src.WaveFormat.SampleRate; return(result); }
/** <summary>Parse the beat code, generating beat cells.</summary> * <param name="beat">Beat code.</param> */ public void Parse(string beat) { ParsedString = beat; // remove comments beat = Regex.Replace(beat, @"!.*?!", ""); if (beat.Contains('$')) { // prep single cell repeat on ref if exists beat = Regex.Replace(beat, @"($[\ds]+)(\(\d\))", "[$1]$2"); //resolve beat referencing while (beat.Contains('$')) { string refBeat; // is a self reference? if (beat[beat.IndexOf('$') + 1].ToString().ToLower() == "s" || Regex.Match(beat, @"\$(\d+)").Groups[1].Value == (Metronome.GetInstance().Layers.Count + 1).ToString()) { refBeat = Regex.Replace(ParsedString, @"!.*?!", ""); } else { //get the index of the referenced beat, if exists int refIndex = int.Parse(Regex.Match(beat, @"\$[\d]+").Value.Substring(1)) - 1; // does referenced beat exist? refIndex = Metronome.GetInstance().Layers.ElementAtOrDefault(refIndex) == null ? 0 : refIndex; refBeat = Regex.Replace(Metronome.GetInstance().Layers[refIndex].ParsedString, @"!.*?!", ""); // remove sound source modifiers for non self references, unless its @0 refBeat = Regex.Replace(refBeat, @"@[a-gA-G]?[#b]?[1-9.]+", ""); } // remove references and their innermost nest from the referenced beat while (refBeat.Contains('$')) { if (Regex.IsMatch(refBeat, @"[[{][^[{\]}]*\$[^[{\]}]*[\]}][^\]},]*")) { refBeat = Regex.Replace(refBeat, @"[[{][^[{\]}]*\$[^[{\]}]*[\]}][^\]},]*", ""); } else { refBeat = Regex.Replace(refBeat, @"\$[\ds]+,?", ""); // straight up replace } } // clean out empty cells refBeat = Regex.Replace(refBeat, @",,", ","); refBeat = Regex.Replace(refBeat, @",$", ""); // replace in the refBeat var match = Regex.Match(beat, @"\$[\ds]+"); beat = beat.Substring(0, match.Index) + refBeat + beat.Substring(match.Index + match.Length); } } // allow 'x' to be multiply operator beat = beat.Replace('x', '*'); beat = beat.Replace('X', '*'); // handle group multiply while (beat.Contains('{')) { var match = Regex.Match(beat, @"\{([^}]*)}([^,\]]+)"); // match the inside and the factor // insert the multiplication string inner = Regex.Replace(match.Groups[1].Value, @"(?<!\]\d*)(?=([\]\(\|,+-]|$))", "*" + match.Groups[2].Value); // switch the multiplier to be in front of pitch modifiers inner = Regex.Replace(inner, @"(@[a-gA-G]?[#b]?\d+)(\*[\d.*/]+)", "$2$1"); // insert into beat beat = beat.Substring(0, match.Index) + inner + beat.Substring(match.Index + match.Length); } // handle single cell repeats while (Regex.IsMatch(beat, @"[^\]]\(\d+\)")) { var match = Regex.Match(beat, @"([.\d+\-/*]+@?[a-gA-G]?[#b]?\d*)\((\d+)\)([\d\-+/*.]*)"); StringBuilder result = new StringBuilder(beat.Substring(0, match.Index)); for (int i = 0; i < int.Parse(match.Groups[2].Value); i++) { result.Append(match.Groups[1].Value); // add comma or last term modifier if (i == int.Parse(match.Groups[2].Value) - 1) { result.Append("+0").Append(match.Groups[3].Value); } else { result.Append(","); } } // insert into beat beat = result.Append(beat.Substring(match.Index + match.Length)).ToString(); } // handle multi-cell repeats while (beat.Contains('[')) { var match = Regex.Match(beat, @"\[([^\][]+?)\]\(?(\d+)\)?([\d\-+/*.]*)"); StringBuilder result = new StringBuilder(); int itr = int.Parse(match.Groups[2].Value); for (int i = 0; i < itr; i++) { // if theres a last time exit point, only copy up to that if (i == itr - 1 && match.Value.Contains('|')) { result.Append(match.Groups[1].Value.Substring(0, match.Groups[1].Value.IndexOf('|'))); } else { result.Append(match.Groups[1].Value); // copy the group } if (i == itr - 1) { result.Append("+0").Append(match.Groups[3].Value); } else { result.Append(","); } } result.Replace('|', ','); beat = beat.Substring(0, match.Index) + result.Append(beat.Substring(match.Index + match.Length)).ToString(); } // fix instances of a pitch modifier being following by +0 from repeater beat = Regex.Replace(beat, @"(@[a-gA-G]?[#b]?[\d.]+)(\+[\d.\-+/*]+)", "$2$1"); BeatCell[] cells = beat.Split(',').Select((x) => { var match = Regex.Match(x, @"([\d.+\-/*]+)@?(.*)"); string source = match.Groups[2].Value; if (Regex.IsMatch(source, @"^[a-gA-G][#b]?\d{1,2}")) { // is a pitch reference return(new BeatCell(match.Groups[1].Value, source)); } else // ref is a plain number. use as pitch or wav file depending on base source. { if (IsPitch) { return(new BeatCell(match.Groups[1].Value, source)); } else { return(new BeatCell(match.Groups[1].Value, source != "" ? WavFileStream.FileNameIndex[int.Parse(source), 0] : "")); } } }).ToArray(); SetBeat(cells); }