using System; using System.Collections; using System.Collections.Generic; using Wavelet_Tracker; namespace Wavelet_Tracker { public class SamplerMachine : MachineParameterisedBase { private const string type = "Sampler"; private const string author = "Internal"; private const int version = 1; private ArrayList playingSounds = new ArrayList(); // A composition node is a handy way to organise track data. private CompositionNode trackStuff = new CompositionNode("foo"); public SamplerMachine(string name) : base(type, author, version, name) { machineLimitations = MachineLimitations.EmitsPCM; } protected override void endProductionImpl(long interval) { for (int x = 0; x < playingSounds.Count; x++) { PlayingSound sound = (PlayingSound)playingSounds[x]; bool hasFinished; PCMEvent evt = sound.play(interval, out hasFinished); outputs[0].buffer.addEvent(evt); if (hasFinished) { playingSounds.RemoveAt(x); x--; } } } protected override void endSeekImpl(long seekTime) { playingSounds.Clear(); } protected override void endSetupParametersImpl() { parameters = new MachineParameter[3]; MachineParameterNote note = new MachineParameterNote("Note", 1); parameters[0] = note; MachineParameterSample sample = new MachineParameterSample("Sample", 1); parameters[1] = sample; MachineParameterContinuous vol = new MachineParameterContinuous("Volume", 1, 1.0f); vol.multiplier = 100; vol.label = "%"; parameters[2] = vol; } protected override void endSetupInputOutputImpl() { outputs = new MachineConnection[1]; outputs[0] = new MachineConnection("Sound Output"); inputs = new MachineConnection[0]; } protected override void endTickImpl(ArrayList parameterChanges) { // We have to handle commands for each track before we launch the notes for those tracks. for (int x = 0; x < parameterChanges.Count; x++) { MachineParameterChange mpc = (MachineParameterChange)parameterChanges[x]; if (mpc.parameter.parameterName.Equals("Sample")) { int value = (int)mpc.newValue; // TODO: Memory leak here trackStuff.setAttribute("Sample " + mpc.trackNum + " " + mpc.playingPatt, value); parameterChanges.RemoveAt(x); x--; } else if (mpc.parameter.parameterName.Equals("Volume")) { float value = (float) mpc.newValue; // TODO: Memory leak here trackStuff.setAttribute("Volume " + mpc.trackNum + " " + mpc.playingPatt, value); parameterChanges.RemoveAt(x); x--; } } for (int x = 0; x < parameterChanges.Count; x++) { MachineParameterChange mpc = (MachineParameterChange) parameterChanges[x]; // This is what the programmer will do for each parameter that they want to watch // for changes. Any parameters that are just read from during production can be // ignored. if (mpc.parameter.parameterName.Equals("Note")) { NoteEvent note = (NoteEvent) mpc.newValue; if (note.note > 0) { double notePitch = ScaleDefinition.getNotePitch(note.scale, note.note, note.fineTune); int sampleNum = 0; if (trackStuff.getAttribute("Sample " + mpc.trackNum + " " + mpc.playingPatt) != null) sampleNum = (int)trackStuff.getAttribute("Sample " + mpc.trackNum + " " + mpc.playingPatt); double sampleVol = 1.0; if (trackStuff.getAttribute("Volume " + mpc.trackNum + " " + mpc.playingPatt) != null) sampleVol = (float)trackStuff.getAttribute("Volume " + mpc.trackNum + " " + mpc.playingPatt); Sample s = App.samples.getSample(sampleNum); if (s != null) { PlayingSound sound = new PlayingSound(playTime, s, notePitch, sampleVol, mpc.trackNum, mpc.playingPatt); playingSounds.Add(sound); //Console.WriteLine(sound.ToString()); } } else { for (int y = 0; y < playingSounds.Count; y++) { PlayingSound sound = (PlayingSound)playingSounds[y]; if (sound.trackNum == mpc.trackNum && sound.playingPatt == mpc.playingPatt) { playingSounds.RemoveAt(y); y--; } } } } } // Finally we have to make sure we clean up after ourselves when patterns end. TODO: Write a helper // class to aid with this. for (int x = 0; x < parameterChanges.Count; x++) { MachineParameterChange mpc = (MachineParameterChange)parameterChanges[x]; if (mpc.parameter.parameterName.Equals("$End$")) { int ppNum = mpc.playingPatt; ArrayList list = trackStuff.getAttributes(); for (int y = 0; y < list.Count; y++) { NodeAttribute att = (NodeAttribute) list[y]; if (att.name.EndsWith("" + ppNum)) { list.RemoveAt(y); y--; } } } } } } public class PlayingSound { // The launch time and playback position of the sample. public long startTime; public long currTime; // Playback position in real time. public long playTime; // The sample we are playing. public Sample sample; // The frequency (of the note) we are playing it at. public double freq; // The volume of the note we are playing. public float volume; public int trackNum; public int playingPatt; public PlayingSound(long playTime, Sample s, double notePitch, double noteVol, int track, int patt) { currTime = 0; startTime = playTime; sample = s; freq = notePitch/440.0; volume = (float) noteVol; trackNum = track; playingPatt = patt; } public unsafe PCMEvent play(long interval, out bool finished) { finished = (currTime > sample.sampleTime); // First we calculate the scaled interval of sound we need to play. long sInterval = (long) (interval * freq); // We ask the sample for sample events (in fact it will only return // one event, a PCM buffer). List list = sample.getSampleEvents(currTime, currTime + sInterval); // Advance the *sample* playback time by the interval calculated. currTime += sInterval; if (list.Count > 0) { PCMEvent theEvent = (PCMEvent)list[0]; // Now we must modify the event so that it is timed correctly // for playback. Notice we do not need to scale the data ourselves. theEvent.startTime = startTime+playTime; theEvent.endTime = startTime+playTime+interval; // We also need to apply the volume to the sample. if (volume < 0.999f && volume >= 0.0f) { for (int x = 0; x < theEvent.bufferLength; x++) { for (int y = 0; y < theEvent.numChannels; y++) { *(theEvent.data[y] + x) *= volume; } } } // Advance the *note* playback time by the real elapsed time. playTime += interval; return theEvent; } return new PCMEvent(0, 100, 1); } public override string ToString() { return "" + startTime; } } }