Skip to content

Instantly share code, notes, and snippets.

@gusano
Created March 10, 2017 02:40
Show Gist options
  • Save gusano/a0b81cce3b7f8114e953c551c3416213 to your computer and use it in GitHub Desktop.
Save gusano/a0b81cce3b7f8114e953c551c3416213 to your computer and use it in GitHub Desktop.
SuperCollider SonaGraph-like implementation, with help file
// an implementation of the spectrum analyzer
// in the Kay Sonogram, as a bank of filters
// with piano-based frequency
// provided with interactive GUIs
// it exports spectral data in a musical format
// AV since 15/11/2016
SonaGraph {
var ampResp, pitchResp, hasPitchResp ;
var <>pitch, <>hasPitch, <>amp, <>anRate ;
var <>buf ;
var <>spectrum, <>maxima ;
var <>sonoChord ;
var <>avHasPitch, <>avPitch ;
var synths, synthRt ;
*prepare {
Server.local.waitForBoot{
SynthDef(\pitch, { arg in, out, freq = 10 ;
var pt, hpt;
#pt, hpt = Lag3.kr(Tartini.kr(In.ar(in))) ;
SendReply.ar(Impulse.ar(freq), '/pitch', values: pt.cpsmidi.round) ;
SendReply.ar(Impulse.ar(freq), '/hasPitch', values: hpt)
}).add ;
SynthDef(\bank, {arg in = 0, out = 0, dbGain = 0, freq = 10, rq = 0.001;
var amp;
var source = In.ar(in,1) * dbGain.dbamp;
amp = Array.fill(88, {|i|
Lag.kr(Amplitude.kr(
BPF.ar(source, (21+i).midicps, rq))
).ampdb}) ;
SendReply.ar(Impulse.ar(freq), '/amp', values: amp)
}).add ;
SynthDef(\sine, {|freq = 440, out = 0, amp = 0.1 |
Out.ar(out, SinOsc.ar(freq, mul:amp)*EnvGen.kr(Env.perc, doneAction:2))
}).add ;
SynthDef(\player, {|buf, start = 0, out =0 |
Out.ar(out, PlayBuf.ar(1, buf, startPos:start))
}).add ;
} ;
SynthDef(\mdaPiano, { |out=0, freq=440, gate=1,
vel = 100, decay = 0.8, thresh = 0.01, mul = 0.1|
var son = MdaPiano.ar(freq, gate, vel, decay,
release: 0.5, stereo: 0.3, sustain: 0);
DetectSilence.ar(son, thresh, doneAction:2);
Out.ar(out, son * mul);
}).add;
SynthDef(\sinePlay, { arg freq, amp;
Out.ar(0, SinOsc.ar(freq, mul:amp))
}).add
}
analyze { |buffer, rate = 10, rq = 0.01|
var x = Synth(\bank, [\freq, rate, \rq, rq]) ;
var y = Synth(\pitch, [\freq, rate]) ;
var anBus = Bus.audio(Server.local, 1) ;
var z = {Out.ar([anBus,0], PlayBuf.ar(1,buffer, doneAction:2))}.play ;
amp = [] ; pitch = []; hasPitch = []; anRate = rate ;
buf = buffer ;
ampResp = OSCFunc({ |msg| amp = amp.add(msg[3..]) }, '/amp');
pitchResp = OSCFunc({ |msg| pitch = pitch.add(msg[3..].postln) }, '/pitch');
hasPitchResp = OSCFunc({ |msg| hasPitch = hasPitch.add(msg[3..].postln) }, '/hasPitch');
x.set(\in, anBus) ; y.set(\in, anBus) ;
{
(buffer.numFrames/Server.local.sampleRate).round.wait;
ampResp.free ; pitchResp.free ;
x.free; y.free ;
//clean up
// avoid -inf
amp = amp.collect{ |i|
if(i.includes(-inf)){
i = Array.fill(88, {-96})}{i}
} ;
// flat and remove strange values
pitch = pitch.flat.postln.collect{|i|
case {i < 21} {i = 21 }
{i > (88+21)} {i = (88+21) }
{(i >=21)&&(i<=(88+21))} {i}}.postln ;
hasPitch = hasPitch.flat.postln ;
amp = amp[..
(buffer.numFrames/Server.local.sampleRate*rate).asInteger
] ;
pitch = pitch[..
(buffer.numFrames/Server.local.sampleRate*rate).asInteger
] ;
hasPitch = hasPitch[..
(buffer.numFrames/Server.local.sampleRate*rate).asInteger
] ;
}.fork
}
// spectral slice methods are intended for short
// as they work by averaging data
// set average pitch of the sound
calculateAvPitch {
avPitch = pitch.sum/pitch.size
}
// set average pitchedness of the sound
calculateAvHasPitch {
avHasPitch = hasPitch.sum/hasPitch.size
}
// spectrum is the average db of the db seq
calculateSpectrum {
// spectrum sum and average, from 21 to 88+21
spectrum = amp.flop.collect{|i| i.sum/i.size}
}
// an interactive GUI, could be moved out
plotSpectrum {|step = 10|
var w = Window.new("spectrum",
Rect(100, 100, step*88, 96*4)).front ;
if (spectrum.isNil){ this.calculateSpectrum } ;
w.drawFunc = {
Pen.font = Font( "DIN Condensed", step );
Array.series(9, -10, -10).do{|i,j|
Pen.strokeColor = Color.gray(0.5) ;
Pen.line(
Point(0, i.neg*4),
Point(w.bounds.width, i.neg*4)
) ;
Pen.stroke ;
Pen.fillColor = Color.red ;
Pen.stringAtPoint(i.asString,Point(0, i.neg*4)) ;
Pen.fill
} ;
Pen.stroke ;
amp.flop.collect{|i| i.sum/i.size}.do{|i,j|
Pen.fillColor = Color.red ;
if(((j+21)%12) == 0){
Pen.stringAtPoint(
((j+21).midinote.last).asString, Point(step*j+step, 10)) ;
Pen.fillStroke ;
Pen.strokeColor = Color.gray(0.5) ;
Pen.line(
Point(step*j, 0),
Point(step*j, 96*4)
) ;
Pen.stroke ;
} ;
Pen.strokeColor = Color.white ;
Pen.line(
Point(step*j, i.neg*4),
Point(step*j, 96*4)
) ;
Pen.stroke ;
Pen.addOval(
Rect(step*j-(step*0.25), i.neg*4, step*0.5, step*0.5)
) ;
Pen.fill ;
Pen.fillColor = Color.black ;
Pen.stringAtPoint((j+21).midinote.toUpper[..1],Point(step*j-(step*0.5), i.neg*4+step)) ;
Pen.stringAtPoint((j+21).asString,Point(step*j-(step*0.5), i.neg*4-step)) ;
Pen.fill
} ;
} ;
w.view.mouseDownAction_{|view, x, y, mod|
var pitch =
x.linlin(0, view.bounds.width, 0, 88).round + 21;
Synth(\mdaPiano, [\freq, pitch.midicps]) ;
}
}
// set the maxima arr as a num of spectral maxima and db
specMaxima { arg num = 4 ;
var amps ;
maxima = [] ;
if (spectrum.isNil){ this.calculateSpectrum } ;
// we do a copy because of sort
amps = spectrum.collect{|i| i};
amps = amps.sort.reverse[..(num-1)] ;
amps.do{|amp|
maxima =
maxima.add([spectrum.indexOf(amp)+21, amp]) ;
} ;
}
// gives you back the chord of maxima, pitches and no dbs
maximaChord { ^maxima.collect{|i| i[0]} }
// plays back the maxima chord, db weighted
playMaxima {|boost = 20| // lotta dbs coz typically low
maxima.do{|i|
Synth(\mdaPiano,
[\freq, i[0].midicps, \mul, (i[1]+boost).dbamp])
};
}
// spec to lily
// PRIVATE
createLilyNote {|midi|
var post = "" ;
var name = midi.midinote[..1] ;
var oct = midi.midinote[2].asString.asInteger ;
name = name.replace(" ", "").replace("#", "is") ;
if (oct >= 5){
(oct-4).do{|i|
post = post++"'"
}
}{
(4-oct).do{
post = post++","
}
};
^name++post++4;
}
createLilyChord {|chord|
var treble = [], tCh = "" ;
var bass = [], bCh = "";
chord.postln.do{|midi|
if (midi >= 60){
treble = treble.add(this.createLilyNote(midi))
}{
bass = bass.add(this.createLilyNote(midi))
}
};
if (treble == []) {
tCh = " \\hideNotes c'4 \\unHideNotes \\override Stem.transparent = ##t"
}{
treble.do{|n| tCh = tCh+n}};
if (bass == []) {
bCh = " \\hideNotes c,4 \\unHideNotes \\override Stem.transparent = ##t"
}{
bass.do{|n| bCh = bCh+n}};
if (bass.size > 0) { bCh = "<<"+bCh+">>" };
if (treble.size > 0) { tCh = "<<"+tCh+">>" };
^[tCh, bCh]
}
writeLilyChord {|chord, path|
var treble = "" ;
var bass = "" ;
var score ;
var lilyChFile, ch ;
path = if (path.isNil){"/tmp/sonoLily.ly"}{path} ;
lilyChFile = File(path,"w") ;
score = "
\\version \"2.18.2\"
\\header {
tagline = \"\" % removed
}
#(set! paper-alist (cons '(\"my size\" . (cons (* 1.5 in) (* 2.5 in))) paper-alist))
\\paper {
#(set-paper-size \"my size\")
}
woodstaff = {
% This defines a staff with only one lines.
% It also defines its position
\\override Staff.StaffSymbol.line-positions = #'(0)
\\override Staff.TimeSignature #'stencil = ##f
\\override Stem.transparent = ##t
\\set fontSize = -3
%\tempo 4 = BPM
\\override Score.MetronomeMark.X-offset = #-3
\\override Score.MetronomeMark.Y-offset = #6
\\clef percussion
\\override Staff.Clef #'stencil = ##f
\\time 1/4
\\hideNotes
% This is necessary; if not entered, the barline would be too short!
\\override Staff.BarLine.bar-extent = #'(-1 . 1)
}
\\score {
<<
\\new PianoStaff
<<
\\new Staff
{\\override Staff.TimeSignature #'stencil = ##f
\\override Stem.transparent = ##t
\\set fontSize = -1
%\tempo UN = BPM
\\time 1/4
TREBLE
}
\\new Staff
{\\clef bass
\\override Staff.TimeSignature #'stencil = ##f
\\override Stem.transparent = ##t
\\set fontSize = -1
%\tempo UN = BPM
\\time 1/4
BASS
\\bar \"|.\"
}
>>
>>
}
" ;
ch = this.createLilyChord(chord) ;
treble = treble + ch[0]++"\n" ;
bass = bass +ch[1] ++"\n" ;
score = score.replace("TREBLE", treble)
.replace("BASS", bass)
;
lilyChFile.write(score);
lilyChFile.close;
}
// PUBLIC
specToLily {|path|
this.writeLilyChord(this.maximaChord, path)
}
renderLily {|path|
path = if (path.isNil){"/tmp/sonoLily.ly"}{path} ;
(
"Applications/LilyPond.app/Contents/Resources/bin/lilypond -fpng --output="++path.splitext[0] + path
).unixCmd
}
showSpec { |num = 6|
var im, w ;
this.specMaxima(num) ;
{
this.specToLily ;
1.wait ;
this.renderLily ;
1.wait ;
im = Image.new("/tmp/sonoLily.png");
w = Window.new("", Rect(400, 400, 100, 180));
w.view.background_(Color.white);
w.view.backgroundImage_(im);
w.front;
w.view.mouseDownAction_{this.playMaxima}
}.fork(AppClock)
}
// END OF SPECTRUM METHODS
// converts the amp seq into a chord seq
sonoToChord {|thresh = -30|
sonoChord =
amp.collect{|slice|
slice.collect{|i,j|
[j+21, i]}
}
.collect{|slice|
slice.select{|pa|
pa[1]>=thresh
}
}
}
// plays it back immediately
// if a note is present in the previous block
// it is not played for sake of intelligibility
playSonoChord { |boost = 15|
{
var playing = [] ;
sonoChord.do{|chord|
if (chord.size>0) {
chord.do{|note|
if (playing.includes(note[0]).not){
Synth(\mdaPiano,
[\freq, note[0].midicps,
\mul, (note[1]+boost).dbamp]
) }
}
} ;
playing = chord.collect{|i| i[0]} ;
anRate.reciprocal.wait
}
}.fork ;
}
// create a voice from a sequence by grouping
// i.e. note with att, dur, db
// asProfile if true-> returns sequence of amps (profile)
makeVoice {|voice, asProfile = false|
var v = Pseq(voice).asStream ;
var next = v.next;
var actual = next;
var time = 0, att = 0, dur = 0 ;
var dyn ;
var notes = [] ;
voice.size.do{
case
{ (next.isNil).and(actual.isNil) }
{ actual = next ; next = v.next; time = time+1 }
// start
{ (next.isNil.not).and(actual.isNil) }
{ att = time ;
dur = 1 ;
dyn = [next] ;
actual = next ; next = v.next; time = time+1 }
// sus
{ (next.isNil.not).and(actual.isNil.not) }
{
dur = dur+1 ;
dyn = dyn++[next] ;
actual = next ; next = v.next; time = time+1 }
// end
{ (next.isNil).and(actual.isNil.not) }
{
if (asProfile)
{notes = notes.add([dyn, att, dur])}
{notes = notes.add([(dyn.sum/dyn.size), att, dur]) } ;
actual = next ; next = v.next; time = time+1 }
} ;
^notes
}
// create all voices from a sonagram
makeVoices {|thresh, asProfile = false|
var voices = amp.flop.collect{|i| i.collect{|i|
if(i < thresh){nil}{i}
}} ;
var vc, vDict = () ;
voices.do{|i,j|
vc = this.makeVoice(i, asProfile) ;
if(vc.size>0){vDict[j] = vc}
} ;
^vDict
}
// writes a midi file, can be used directly
// as it calls makeVoices
voicesToMidi {|path, voices, thresh|
var m = SimpleMIDIFile(path);
// create empty file
m.init1( 1, 120, "4/4" );
// init for type 1 (multitrack); 3 tracks, 120bpm, 4/4 measures
m.timeMode = \seconds;
// change from default to something useful
voices = if (voices.isNil){this.makeVoices(thresh)}{voices} ;
voices.keys.do{|key,j|
voices[key].do {|ev|
[key, ev].postln ;
m.addNote(
key+21,
ev[0].linlin(-96,0, 0, 127).asInteger,
ev[1]*anRate.reciprocal,
ev[2]*anRate.reciprocal
)} ;
} ;
m.write
}
// synthesize sonagram
synthesize { arg thresh ;
thresh = if (thresh.isNil) {-96.neg}{thresh} ;
synthRt = {
synths = Array.fill(88, {Synth(\sinePlay)}) ;
amp.do{|bl|
bl.do{|v, i|
v = if (v >= thresh){ v }{ -96} ;
synths[i].set(
\freq, (21+i).midicps,
\amp, v.dbamp)
} ;
anRate.reciprocal.wait ;
};
synths.do{|i| i.free}
}.fork;
^synthRt
}
// clean out
stopSynthesize{
synthRt.stop; synths.do{|i| i.free}
}
writeArchive { |path|
[amp, pitch, hasPitch, anRate].writeArchive(path)
}
readArchive {|path|
#amp, pitch, hasPitch, anRate = Object.readArchive(path)
}
// redirect and helper
gui {|buffer, hStep = 2.5, vStep = 6, thresh|
var bf = case
{ buffer.notNil }{ buffer}
{ buffer.isNil }{ buf } ;
thresh = if(thresh.isNil){-96}{thresh} ;
if (bf.isNil){"Please pass a buffer!".postln}{
SonaGraphGui(this, bf, hStep, vStep).makeGui(thresh)
}
}
postScript { arg path, buffer, width = 600, height = 200,
frame = 30,
xEvery = 1,
xGridOn = true, yGridOn = true,
xLabelOn = true, gridCol = Color.red(0.6),
frameCol = Color.green(0.5),
cellType = \oval;
var grCol = [gridCol.red, gridCol.green, gridCol.blue] ;
var frCol = [frameCol.red, frameCol.green, frameCol.blue] ;
var bf = case
{ buffer.notNil }{ buffer}
{ buffer.isNil }{ buf } ;
if (bf.isNil){"Please pass a buffer!".postln}{
this.ps(path, bf, width:width, height:height,
frame:frame,
xEvery: xEvery, xGridOn:xGridOn,
yGridOn:xGridOn, xLabelOn:xLabelOn,
gridCol: grCol, frameCol:frCol, cellType:cellType)
}
}
ps {
arg path, buf, width = 600, height = 200, frame = 30,
xEvery = 2, xLabEvery = 2,
xGridOn = true, yGridOn = true,
xLabelOn = true, gridCol = [1, 0, 0], frameCol = [0.2,0.5,0.7],
cellType = \oval;
var dur = buf.numFrames/Server.local.sampleRate ;
var xEv = xEvery.linlin(0, dur, 0, amp.size) ;
//var xLM = xEv.linlin(0, amp.size, 0, dur);
PsSonaGraph(amp, path, width:width, height:height,
frame: frame,
xEvery: xEv, xGridOn:xGridOn,
xLabMul: xEvery,
yGridOn:xGridOn, xLabelOn:xLabelOn,
gridCol: gridCol, frameCol:frameCol,
cellType:cellType
) ;
}
}
// GUI class for plotting and interactive usage
SonaGraphGui {
var <>sonaGraph ;
var <>sf, <>buffer ;
var <>amp, <>pitch, <>anRate ;
var <>thresh ;
var <>w, <>u ; // user window for sonagraph
var <>hStep, <>vStep ;
var <>cursor, <>cursorView, <>display, <>sfView;
*new { arg sonaGraph, buffer, hStep = 2.5, vStep = 6 ;
^super.new.initSonaGraphGui(sonaGraph, buffer, hStep, vStep)
}
initSonaGraphGui { arg aSonaGraph, aBuffer,
aHStep, aVStep ;
sonaGraph = aSonaGraph ;
buffer = aBuffer ;
hStep = aHStep; vStep = aVStep ;
sf = SoundFile.new ;
sf.openRead(buffer.path) ;
amp = sonaGraph.amp;
pitch = sonaGraph.pitch;
anRate = sonaGraph.anRate;
}
makeGui { |thresh = -60, sfViewH = 100|
// thresh: used to select active band AND pitch
var flag, player ;
w = Window("SonaGraph",
Rect(10, 100, hStep*amp.size, vStep*amp[0].size+sfViewH))
.background_(Color.gray)
.front ;
u = UserView(w, Rect(0, 0, hStep*amp.size, vStep*amp[0].size))
.background_(Color.white)
.drawFunc_{
88.do{|i|
if (i%12 == 0){
Pen.strokeColor_(Color.hsv(0.1, 0.2, 1)) ;
Pen.line(0 @ (vStep*(i+1)), u.bounds.width @ (vStep*(i+1)) ) ;
Pen.stroke
}
} ;
amp.do{|pitchArr,i|
pitchArr.do{|p, j|
if (p > thresh){
Pen.fillColor_(Color.gray(p.linlin(thresh, -10, 0.9, 0))) ;
Pen.fillOval(Rect(i*hStep,
vStep*amp[0].size-(j*vStep)-(vStep*0.5),
hStep, vStep))
}
}
} ;
Pen.fillColor_(Color.red) ;
pitch.do{|p, i|
if(amp[i][p-21] > thresh){
Pen.fillOval(Rect(i*hStep,
vStep*amp[0].size-((p-21)*vStep)-(vStep*0.5),
hStep, vStep))
}
}
} ;
cursor = [0,0]; flag = true ;
cursorView = UserView(w, Rect(0, 0, hStep*amp.size, vStep*amp[0].size))
.background_(Color(1,1,1,0));
display = StaticText(w, Rect(5, 5, 200, 15))
.font_(Font("DIN Condensed", 12)).align_(\left) ;
cursorView.mouseDownAction_{|view, x, y, mod|
var pitch = ((88+21)-(y/vStep)).round ;
//var time = (x/amp.size).round(0.001) ;
// should be related to buf dur
var time = x.linlin(0, u.bounds.width,
0, buffer.numFrames/Server.local.sampleRate).round(0.001) ;
Synth(\sine, [\freq, pitch.midicps]) ;
display.string_((time.asString++" M:"
++pitch.asString
++" N:"++pitch.midinote.capitalize.replace(" ", "")
++" F:"++pitch.midicps.round(0.01).asString)
);
cursor = [x,y];
cursorView.refresh ;
}
.drawFunc_{
//~display.bounds_(Rect(~cursor[0]+3, ~cursor[1]+3, 100, 15));
display.bounds_(Rect(cursor[0]-23, cursorView.bounds.height-15, 200, 15));
Pen.strokeColor = Color.red ;
Pen.line(0 @ cursor[1], cursorView.bounds.width @ cursor[1]);
Pen.line(cursor[0] @ 0, cursor[0] @ cursorView.bounds.height);
Pen.stroke
}
.keyDownAction_{ |doc, char, mod, unicode, keycode, key|
if ((unicode == 32)&&(flag == true)){
flag = false ;
player = Synth(\player, [\buf, buffer, \start,
cursor[0].linlin(0, cursorView.bounds.width, 0, sf.numFrames)])
}
{
flag = true;
player.free;
}
} ;
sfView = SoundFileView.new(w, Rect(0, cursorView.bounds.height, hStep*amp.size, sfViewH))
.gridColor_(Color.gray(0.3)) ;
sfView.soundfile = sf; // set soundfile
sfView.read(0, sf.numFrames); // read in the entire file.
sfView.refresh;
}
}
/*
PsSonaGraph generates a postscript/pdf file with a plot
from amp data structure of SonaGraph
Andrea Valle scripsit 15/11/2016
*/
PsSonaGraph {
var <>str ; // the string
var <>path ; // where to write it
var <>width, <>height ; // fig dimensions in points
var <>xEvery, <>frame ; // x and y grid, frame in pt
var <>yGridOn, <>xGridOn ; // plot grid?
var <>gridWidth, <>frameWidth, <>curveWidth ; // stroke widths
var <>gridCol, <>frameCol, <>curveCol ; // stroke widths
var <>speckleWidth, <>speckleCol ;
var <>barWidth, <>barCol ;
var <>data, <>thresh, <>rate;
var <>fontName, <>fontSize, <>fontCol ;
var <>yLabelOn, <>xLabelOn ;
var <>xLabRound, <>yLabRound ;
var <>xLabMul;
var <>xLabStart, <>yLabStart ;
var <>xLabEvery, <>yLabEvery ;
var <>samplePoints ; // number of point for resampling
var <>duration ; // duration to be calculated for functions
var <>zero, <>zeroWidth ;
var <>name, <>ext ;
var <>cellType ;
// first we assume data is an array
*new { arg
data, path, thresh= -60, rate = 10,
width = 400, height = 200,
xEvery = 25, frame = 30,
yGridOn = true, xGridOn = true,
gridWidth = 0.2, frameWidth = 1,
gridCol = [0.5, 0.5, 0.5],
frameCol = [0,0,0], curveCol = [0,0,0],
fontName = "Courier", fontSize = 6, fontCol = [0,0,0],
yLabelOn = true, xLabelOn = true,
xLabRound = 0.01, yLabRound = 0.01,
xLabMul = 1,
xLabStart = -10, yLabStart = -25,
xLabEvery = 2, yLabEvery = 2, cellType = \oval ;
// this is ugly
^super.new.initPsPlotter(
data, path, thresh,rate,
width, height, xEvery, frame, yGridOn, xGridOn,
gridWidth, frameWidth,
gridCol, frameCol,
fontName, fontSize, fontCol,
yLabelOn, xLabelOn,
xLabRound , yLabRound,
xLabMul,
xLabStart, yLabStart, xLabEvery, yLabEvery,
cellType
)
}
initPsPlotter { arg
// this can't be looked at
adata, apath, athresh, arate,
awidth, aheight, axEvery, aframe, ayGridOn, axGridOn,
agridWidth, aframeWidth,
agridCol, aframeCol,
afontName, afontSize, afontCol,
ayLabelOn, axLabelOn,
axLabRound, ayLabRound,
axLabMul,
axLabStart, ayLabStart,
axLabEvery, ayLabEvery,
aCellType;
var arr ;
# data, path, thresh, rate,
width, height, xEvery, frame, yGridOn, xGridOn,
gridWidth, frameWidth,
gridCol, frameCol,
fontName, fontSize, fontCol,
yLabelOn, xLabelOn,
xLabRound , yLabRound,
xLabMul,
xLabStart, yLabStart, xLabEvery, yLabEvery,
cellType
=
[adata, apath, athresh,arate,
awidth, aheight, axEvery, aframe, ayGridOn, axGridOn,
agridWidth, aframeWidth,
agridCol, aframeCol,
afontName, afontSize, afontCol,
ayLabelOn, axLabelOn,
axLabRound, ayLabRound,
axLabMul,
axLabStart, ayLabStart,
axLabEvery, ayLabEvery,
aCellType
];
#name, ext = path.split($.) ;
str = "" ;
this.plot ;
this.write
}
cell {|x,y,width,height, gray|
if (cellType == \oval){
^this.circleCell(x, y, width, height, gray)}
{ ^this.squareCell(x, y, width, height, gray)}
}
squareCell {|x, y, width, height, gray|
var cell = "newpath
X Y moveto
0 height rlineto
width 0 rlineto
0 -height rlineto
closepath
GR setgray
fill
"
.replace("X", x).replace("Y", y)
.replace("height", height).replace("width", width)
.replace("GR", gray) ;
^cell
}
circleCell {|x,y,width, height, gray|
var ratio = height/width ;
var cell = "gsave
ratio 1 scale
X Y width 0 360 arc
GR setgray
fill
grestore
"
.replace("X", x*ratio.reciprocal).replace("Y", y)
.replace("width", width*0.5).replace("ratio", ratio)
.replace("GR", gray) ;
^cell
}
setWidth {
str = str++"% document size\n";
str = str++"<< /PageSize [X Y] >> setpagedevice\n"
.replace("X", width+(frame*2))
.replace("Y", height+(frame*2))
}
setFont {
str = str++"% font setting grid\n";
str = str++"/"++fontName ++" findfont "++ fontSize.asString++" scalefont setfont\n" ;
}
drawFrame {
str = str++"% frame\n";
str = str++frame.asString++" "++frame.asString++" translate\n" ;
str = str++frameWidth.asString++" setlinewidth\n" ;
frameCol.do{|i| str = str++i.asString++" "} ;
str = str++"setrgbcolor\n" ;
str = str++"newpath\n0 0 moveto\n";
str = str++width.asString++" "+ 0.asString++" lineto\n";
str = str++width.asString++" "+ height.asString++" lineto\n";
str = str++0.asString++" "+ height.asString++" lineto\n";
str = str++0.asString++" "+ 0.asString++" lineto\n";
str = str++"closepath stroke\n" ;
}
drawYGrid {
var step = height/88 ;
str = str++"% horizontal grid\n";
str = str++gridWidth.asString++" setlinewidth\n" ;
gridCol.do{|i| str = str++i.asString++" "} ;
str = str++"setrgbcolor\n" ;
str = str++"newpath\n";
88.do{|i|
if (i%12 == 0){
str = str++"-3 "++ (height-(step*(i+1))).asString ++" moveto\n" ;
str = str++(width+2).asString++" "++(height-(step*(i+1))).asString++" lineto\n" ;
}
} ;
str = str++"closepath stroke\n" ;
}
drawYLabels {
var step = height/88 ;
var j = 0 ;
var frq = Array.series(8, 21+88-1, -12).midicps.round(0.01) ;
str = str++"% horizontal labels\n";
fontCol.do{|i| str = str++i.asString++" "} ;
str = str++"setrgbcolor\n" ;
str = str++(-14).asString++" "++(height+fontSize).asString++" moveto\n" ;
str = str ++"(Oct) show\n" ;
str = str++(width+2).asString++" "++(height+fontSize).asString++" moveto\n" ;
str = str ++"(Hz) show\n" ;
str = str++"newpath\n";
88.do{|i|
if (i%12 == 0){
str = str++"-8 "++ (height-(step*(i+1))).asString ++" moveto\n" ;
str = str++"(@) show\n".replace("@", (9-j).asString) ;
str = str++(width+2).asString++" "++(height-(step*(i+1))).asString ++" moveto\n" ;
str = str++"(@) show\n".replace("@", (frq[j]).asString) ;
j = j+1
}
} ;
str = str++"closepath stroke\n" ;
}
// maybe some glitch in time calculation
drawXGrid {
var num, x ;
str = str++"% vertical grid\n";
str = str++gridWidth.asString++" setlinewidth\n" ;
gridCol.do{|i| str = str++i.asString++" "} ;
str = str++"setrgbcolor\n" ;
str = str++"newpath\n";
num = (data.size/xEvery).trunc ;
(num+1).do{|i|
x = i*(width/num);
str = str++ x.asString ++" -3 moveto\n" ;
str = str++x.asString++" "++height.asString++" lineto\n"
} ;
str = str++"closepath stroke\n" ;
}
drawXLabels {
var num, x, val ;
str = str++"% vertical labels\n";
fontCol.do{|i| str = str++i.asString++" "} ;
str = str++"setrgbcolor\n" ;
str = str++(width*2/5).asString++" "++(xLabStart*2).asString++" moveto\n" ;
str = str ++"(time (seconds)) show\n" ;
num = (data.size/xEvery).trunc ;
(num+1).do{|i|
x = i*(width/num);
val = i*xEvery ;
//.asTimeString;
str = str++ (x-(val.asString.size*0.5*fontSize*0.25)).asString ++" "++xLabStart.asString++" moveto\n" ;
str = str++"("++(i*xLabMul)++") show\n"
} ;
}
drawSonagraph {
// curve
var xStep = width/data.size ;
var yStep = height/data[0].size ;
var gray ;
data.do{|ampArr,i|
ampArr.do{|p, j|
if (p > thresh){
gray = p.linlin(thresh, -10, 0.9, 0) ;
str = str++
this.cell(i*xStep, j*yStep, xStep, yStep, gray)
//this.circleCell(i*xStep, j*yStep, xStep, yStep, gray)
}
}
} ;
}
// total
plot {
this.setWidth ;
this.setFont ;
this.drawFrame ;
this.drawSonagraph ;
if (xGridOn) { this.drawXGrid } ;
if (xLabelOn) { this.drawXLabels };
if (yGridOn) { this.drawYGrid } ;
if (yLabelOn) { this.drawYLabels };
}
write {
var file = File(name++".ps", "w") ;
file.write(str++"showpage\n") ;
file.close ;
if (ext.asSymbol == \pdf) {
("pstopdf"+name++".ps").unixCmd{
("rm"+name++".ps").unixCmd
}
}
}
}
/*
(
a = SonaGraph.new;
a.readArchive("/Users/andrea/Desktop/sonaChal.log")
~data = a.amp ;
p = PsSonaGraph(~data, ~sample, "/Users/andrea/Desktop/untitled.pdf", width:600, xEvery: 30, xLabEvery:2, xGridOn:true, yGridOn:true, xLabelOn:true, gridCol: [1, 0, 0], frameCol:[0.2,0.5,0.7]) ;
)
*/
/*
// here we start up server and defs
SonaGraph.prepare ;
// something to analyze, i.e a buffer
~path ="/Users/andrea/musica/recordings/audioRumentario/pezzi/indie_I/fiati/chalumeau or.wav"; ~sample = Buffer.read(s, ~path).normalize ;
// an istance
a = SonaGraph.new;
// now analyzing in real-time
a.analyze(~sample,30) ; // high rate!
// checking size of coupled arrays
// a.amp.size == a.pitch.size ; // true
// here we calculate and plot the average spectrum
//a.plotSpectrum
// GUI is interactive, click and you here the sound
// writing to an archive
a.writeArchive("/Users/andrea/Desktop/sonaChal.log")
a.gui(hStep:1) ; // directly, if anRate=1 then default hStep fine
// again
a = SonaGraph.new ;
// read the log, may require some time
a.readArchive("/Users/andrea/Desktop/sonaChal.log") ;
a.gui(~sample, 1) ; // we still need the sample for playback
// same as:
g = SonaGraphGui(a, ~sample,1).makeGui
// resynthesis
a.synthesize ; // start synthesis
a.stopSynthesize ; // stop synth routine and free
// write to postscript
a.postScript("/Users/andrea/Desktop/sonaChal.pdf") ;
// using spectrum data
// here we start up server and defs
SonaGraph.prepare ;
// something to analyze, i.e a buffer
~path ="/Users/andrea/musica/regna/fossilia/compMine/erelerichnia/fragm/snd/vareseOctandreP18M5N[8,9,0,7,11,6].aif"; ~sample = Buffer.read(s, ~path).normalize ;
// an istance
a = SonaGraph.new;
// now analyzing in real-time
a.analyze(~sample,15) ; // high rate!
a.gui(hStep:10)
a.plotSpectrum
a.showSpec(4)
a.specMaxima(6) // explictly calculating
a.maximaChord // access
a.playMaxima
a.hasPitch
a.pitch
a.calculateAvPitch
a.calculateAvHasPitch
a.avHasPitch
a.avPitch
a.specToLily("/Users/andrea/musica/regna/fossilia/compMine/erelerichnia/chords.ly")
// something to analyze, i.e a buffer
~path ="/Users/andrea/musica/regna/fossilia/compMine/erelerichnia/fragm/octandreExc2.aif"; ~sample = Buffer.read(s, ~path).normalize ;
// an istance
a = SonaGraph.new;
// now analyzing in real-time
a.analyze(~sample,15) ; // high rate!
a.gui(hStep:6) ;
a.sonoToChord(thresh:-40) ; // here you get chord of peaks above thresh
a.sonoChord ; // which is
a.playSonoChord // play it
v = a.makeVoices(-40) ; // reconstructs events from bins above -40 thresh
// write it down to MIDI
a.voicesToMidi("~/Desktop/midifiletest.mid", thresh:-40 ) ;
*/
TITLE:: SonaGraph
summary:: A piano-tuned spectrum analyzer/visualizer, inspired by Kay Sonagraph
categories:: Analysis
related:: Classes
DESCRIPTION::
The classic Kay Sonagraph was based on a bank of filters used to plot on paper spectral information, widely used in phonetics and acoustic analysis (bird singing). SonaGraph works in two steps. First, a sound is analized by passing it through a 88 band pass filter, tuned on piano keys. Amps in dB and pitch are collected and data are then available to manipulation/visualization. Second, data can be explored interactively by the included GUI.
CLASSMETHODS::
METHOD:: prepare
An init method that is used to boot the server and load the required SynthDefs.
INSTANCEMETHODS::
METHOD:: amp
An array of amplitude in dB, as a set of time slices, its size depending on the anRate (see) argument.
returns:: an array of arrays.
METHOD:: pitch
Data structure for pitch.
returns:: An array of pitches, size depending on anRate.
METHOD:: buf
The buffer to be analyzed
METHOD:: analyze
Starts analysis in real-time. Once buffer reading is completed, analysis stop.
The method sets both buffer and anRate vars.
ARGUMENT:: buffer
the buffer to be analysed.
ARGUMENT:: rate
The rate at which analysis polls data.
ARGUMENT:: rq
The rq for the filters, default is 0.01, which typically works fine.
METHOD:: anRate
The rate expressed as a frequency (Hz) for amplitude/pitch sampling. The final size of data structure depends on anRate x duration in seconds of the buffer.
METHOD:: synthesize
Resynthesizes the amp array by means of sinusoids.
METHOD:: stopSynthesize
Stops synthesizing.
METHOD:: writeArchive
Writes the data structure to a file, so that it can be retrieved again.
ARGUMENT:: path
Path to the file.
METHOD:: readArchive
Retrieve and set a previously archived data structure.
ARGUMENT:: path
Path to the file.
METHOD:: gui
Creates a GUI for interactive exploration by calling internally a dedicated class (SonaGraphGui). Note that the gui hasn't scroll. When you press on the view, a sine of the related freq is played, and time, note and freq are displayed. By pressing the space bar, sound is played back from the cursor position. To stop playback, press again the space bar.
ARGUMENT:: buffer
The buffer to be played back, i.e. the one used for analysis. If gui is created while the instance for analysis is still on, buffer can be nil and the previously passed one is used.
ARGUMENT:: hStep
Horizontal step. As there is no zoom feature, anRate x duration x hStep gives the overall width of the gui. Thus, hStep must be set according to your screen dimension.
ARGUMENT:: vStep
Vertical step for each cell (height = vStep x 88).
METHOD:: postScript
Allows to generate a PostScript file of the sonagram. It includes labelling. On y axis labels represent octave and Hz on opposite side.
ARGUMENT:: path
Path of the file, if extension is ps a PostScript file is generated, if pdf the method calls pstopdf from Terminal, creates a pdf, and remove the generated ps file. If pstopdf is not accessible, you can write ps and convert by hand.
ARGUMENT:: width
Width (+frame) in pixel of the PostScript file.
ARGUMENT:: height
Height (+frame) in pixel of the PostScript file.
ARGUMENT:: frame
Frame in pixel for the drawing, it is added to both height and width.
ARGUMENT:: xEvery
Grid spacing on x axis, in seconds.
ARGUMENT:: xGridOn
Allows to disable vertical grid plotting.
ARGUMENT:: yGridOn
Allows to disable horizontal grid plotting.
ARGUMENT:: xLabelOn
Allows to disable vertical label plotting.
ARGUMENT:: gridCol
a Color for the grid.
ARGUMENT:: frameCol
a Color for the frame.
ARGUMENT:: cellType
if \oval fill each cell with an oval, else with a filled rect. Default is \oval.
EXAMPLES::
code::
// here we start up server and defs
SonaGraph.prepare ;
// something to analyze, i.e a buffer
~path = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
~sample = Buffer.read(s, ~path).normalize ;
// an istance
a = SonaGraph.new ;
// now analyzing in real-time
a.analyze(~sample,50) ; // high rate! 10 could be enough, depends on dur
// writing to an archive, log extension is not necesssary
a.writeArchive("/Users/andrea/Desktop/a11.log") ;
a.gui(hStep:5) ; // directly, if anRate=1 then default hStep 2.,5 fine
// again
a = SonaGraph.new ;
// read the log, may requires some time
a.readArchive("/Users/andrea/Desktop/a11.log") ;
a.gui(~sample, 5) ; // now we need the pass the sample for playback
// same as:
g = SonaGraphGui(a, ~sample,5).makeGui ;
// resynthesis
a.synthesize ; // start synthesis
a.stopSynthesize ; // stop synthesis routine and free
// postscript generation
a.postScript("/Users/andrea/Desktop/a11.ps", ~sample, xEvery:0.25) ;
// directly pdf
a.postScript("/Users/andrea/Desktop/a11.pdf", ~sample, xEvery:0.25) ;
::
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment