if 0 { music::drawKeyboard <canvas> <keywidth> <keyheight> <nkeys> music::drawNoteLines <canvas> <x0> <y0> <dy> <width> music::drawNote <canvas> <note> music::getFrequency <note> music::play <string> ;# (a list of notes and other markup) music::playNote <note> <ms> }if 0 {If ms is -1, the note starts playing (e.g. after pressing a key). The sound is turned off again by calling with ms = 0. "Note" above refers to a string consisting of maximally four parts:
- base note: [A-Ga-g], cover two octaves; x for pause
- optional sign: [#bB]: b only after b, B only after B
- optional octave marker: 1,2 go down, one to three 's go up
- optional length marker: + double, - half, .: 1.5 times
- "x" for pauses
- ">", "<" for piano/forte (low or high amplitude) changes
- "/" signs (bars) have no effect, except of aiding the reader
package require sound ;# snack without Tk namespace eval music { variable version 0.1 ;# well yes, with some iterations ;-) variable A 440 ;# standard pitch variable amplitude 20000 variable basicNames {c c# d d# e f f# g g# a bb b} variable bpm 72 variable dampInterval 100 ;# ms for damping steps variable dampConstant 0.3 variable freqMap ;# array (notename) -> frequency variable showNotes 0 ;# default for Tcl variable snackRate 22050 ;# sampling of sound objects variable snackShape 0.5 variable snackType sine ;# could also be rectangle or triangle }#--------------------------------------------------- Sound rendering
proc music::play {score {Tk 0}} { variable amplitude set t 0 foreach item $score { switch -- $item { / {# bar ignored} < {after $t set music::amplitude [expr {$music::amplitude*2}]} > {after $t set music::amplitude [expr {$music::amplitude/2.}]} default { set dt [getDuration $item] after $t music::playNote $item $dt $Tk incr t $dt } } } } proc music::playNote {note {duration ""} {Tk 0}} { variable current $note variable showNotes set f [getFrequency $note] if {$f==""} {error "unknown note $note"} if {$duration==""} {set duration [getDuration $note]} if {$duration} {set ::last [playBegin $f]} if {$duration>=0} { set cmd "music::playEnd $::last" if {$Tk} { keyboardHilite $note 1 append cmd "; music::keyboardHilite $note 0" } after [expr {$duration/2}] $cmd } if {$showNotes && $duration >= 0} {drawNote $note} } proc music::playBegin {freq} { variable amplitude; variable snackShape; variable snackType variable snackRate set shape [expr {$freq<700? 0.95: $snackShape}] set soundname [snack::sound -rate $snackRate] variable $soundname; upvar 0 $soundname sound set filter [snack::filter generator $freq $amplitude\ $shape $snackType] if {$freq} {$soundname play -filter $filter} set sound [list $filter $freq $amplitude] set soundname } proc music::playEnd {{varName ""}} { variable dampConstant variable dampInterval if {$varName==""} {set varName $::last} variable $varName; upvar 0 $varName sound foreach {filter freq ampl} $sound break set a $ampl set dt $dampInterval set t 0 ;# abstract integer units if {$dampConstant <= 0} {set dampConstant 0.1} ;# avoid lock/crash while {$a > 50} { set a [expr {$ampl * exp(-$dampConstant * $t)}] after [expr {$t*$dt}] [list $filter configure $freq $a] incr t 1 } after [expr {$t*$dt}] " $varName stop $filter destroy $varName destroy unset music::$varName" } proc music::getDuration {note} { variable bpm set res [expr {60000/$bpm}] while {[regexp {(.+)[+]$} $note -> note]} { set res [expr {$res*2}] } while {[regexp {(.+)[-]$} $note -> note]} { set res [expr {$res/2}] } if {[regexp {(.+)[.]$} $note -> note]} { set res [expr {round($res*1.5)}] } set res } proc music::getFrequency {note} { variable freqMap set pureName [string trimright $note {+-.}] if {[info exists freqMap($pureName)]} { set freqMap($pureName) } ;# otherwise implicitly returns an empty string } proc music::_makeFreqMap {} { variable A variable basicNames variable freqMap set lda [expr {log($A)/log(2)}] set i 3 ;# C is 3 half-tones above A set freqMap(x) 0 ;# pause foreach name $basicNames { set f [expr {pow(2, $lda + $i/12.)}] set freqMap($name) $f set freqMap($name') [expr {$f*2}] set freqMap($name'') [expr {$f*4}] set uname [string toupper $name] set freqMap($uname) [expr {$f/2.}] set freqMap(${uname}1) [expr {$f/4.}] set freqMap(${uname}2) [expr {$f/8.}] incr i } } music::_makeFreqMap ;# proc'ed only to hide local variablesif 0 { #--------------------------Alternate midi-based music generatorBrian Theado 14Aug04 - Here is an alternate music generator that makes use of the tclmidi package (see midi) which works in Windows. On my computer, the midi synthesizer in the sound card sounds excellent (to my untrained ear). The default instrument is a piano.}
if {![catch { package require midi midi::openout 0 }]} { proc freqToNote {freq} { # Converts the given frequency to a midi note # Midi notes range from 0 to 127 with the lowest note # at a frequency of 8.175 Hz and the highest note at 12557 Hz # Each octave consists of 12 notes and from one octave to the # next, the frequency doubles return [expr round((log($freq/8.175)/log(2)) * 12)] } proc music::playBegin {freq} { set note [freqToNote $freq] # Channel 1 note on at volume 60 midi::sendshort 144 $note 60 return $note } proc music::playEnd {{varName ""}} { if {$varName==""} {set varName $::last} # Channel 1 note off and release the note relatively slowly (the # 5 could be as high as 127 for a quick release of the sound) midi::sendshort 128 $varName 5 } }#-----------------------------------------------Tk stuff: piano keyboard
proc music::drawKeyboard {c x0 y0 dx dy nkeys} { variable current variable kbdCanvas $c set y1 [expr {$y0+$dy}] set y05 [expr $y1*.67] ;# length of black keys set dx2 [expr {$dx/2}] ;# offset of black keys set nkey 0 foreach note [noteSequence] { if {[incr nkey]>$nkeys} break set keycolor [keyColor $note] if {$keycolor=="black"} { set x [expr {$x0 - $dx*.35}] set id [$c create rect $x $y0 [expr {$x+$dx*0.6}] $y05 \ -fill $keycolor -tag [list $note black]] } else { set id [$c create rect $x0 $y0 [expr $x0+$dx] $y1 \ -fill $keycolor -tag $note] incr x0 $dx; incr x0 1 } $c bind $id <1> "music::TkOn $c $id $note" ;# sound on $c bind $id <ButtonRelease-1> "music::TkOff $c $id $note";# sound off $c bind $id <3> \ "set music::current {$note: [format %.1f [getFrequency $note]] Hz}" $c bind $id <Enter> "set music::current $note" $c bind $id <Leave> "set music::current {}" } $c raise black set maxx [lindex [$c bbox all] 2] if {[$c cget -width]<$maxx} {$c config -width [expr {$maxx}]} set maxy [lindex [$c bbox all] 3] if {[$c cget -height]<$maxy} {$c config -height [expr {$maxy}]} } proc music::TkOn {canvas id note} { variable startTime [clock clicks -millisec] $canvas move $id -1 -1 ;# animate the key to look depressed playNote $note -1 } proc music::TkOff {canvas id note} { variable record; variable recorded variable startTime $canvas move $id 1 1 set dt [expr {[clock clicks -millisec] - $startTime}] if {$dt<130} { append note - } elseif {$dt>600} { append note ++ } elseif {$dt>300} { append note + } playNote $note 0 if {$record} {lappend recorded $note} } proc music::keyboardHilite {note mode} { variable kbdCanvas set note [string trimright $note {+-.}] set id [$kbdCanvas find withtag $note] set fill [expr {$mode? "green": [keyColor $note]}] $kbdCanvas itemconfig $id -fill $fill } proc music::keyColor {note} { expr {[regexp -nocase {#|bb} $note]? "black" : "white"} } proc music::noteSequence {} { variable basicNames set ubasic [string toupper $basicNames] foreach i $ubasic {lappend noteSequence ${i}2} foreach i $ubasic {lappend noteSequence ${i}1} foreach i $ubasic {lappend noteSequence ${i}} foreach i $basicNames {lappend noteSequence $i} foreach i $basicNames {lappend noteSequence $i'} foreach i $basicNames {lappend noteSequence $i''} set noteSequence ;# for conveniently creating the keyboard }#------------------------------------------- Tk stuff: Note rendering
proc music::drawLines {canvas x0 y0 x1 dy} { variable noteMap variable scoreCanvas $canvas variable showNotes 1 set noteMap(topY) $y0 foreach i {1 2 3 4 5} { $canvas create line $x0 $y0 $x1 $y0 incr y0 $dy } set noteMap(btmY) [expr {$y0-$dy}] set noteMap(newX) 600 ;# position where new notes are inserted array set noteMap [makeNoteTable [expr $y0-$dy/2] [expr {$dy/2}]] } proc music::drawNote {name} { variable noteMap variable scoreCanvas set c $scoreCanvas regexp {([A-Ga-gx])([Bb#])?[12']*([-+.]*)} $name -> note sign length if {$note=="x"} return ;# pause signs will come later foreach i {1 2} { ;# This is slightly wasteful, but makes the $c move note -9 0 ;# movement of notes better visible. update idletasks ;# Move once by 16 if this causes problems. } set y $noteMap($note) if {[string first 1 $name]>0} {incr y 21} ;# low note if {[string first 2 $name]>0} {incr y 42} ;# very low note while {[regexp (.+)' $name -> name]} {incr y -21} ;# high note set newX $noteMap(newX) set sx [expr {$newX+2}] switch -- $sign { # {$c create text $sx $y -text # -tag note;$c move note -8 0} B - b {$c create text $sx $y -text b -tag note;$c move note -8 0} } set y2 [expr {(($y+3)/6)*6+1}] set ax0 [expr {$newX-2}] ;#--------- auxiliary lines, above or below set ax1 [expr {$newX+11}] while {$y2 < $noteMap(topY)-1} { if {$y<$y2} {$c create line $ax0 $y2 $ax1 $y2 -tag note} incr y2 6 } while {$y2 > $noteMap(btmY)} { $c create line $ax0 $y2 $ax1 $y2 -tag note incr y2 -6 } set newX1 [expr {$newX+8}] set fill black if {[string first + $length]>=0} {set fill {}} $c create oval $newX $y $newX1 [expr {$y+5}] -tag note \ -fill $fill if {[string first . $length]>=0} { $c create text $newX1 $y -anchor w -text " ," -tag note } if {[string first ++ $length]<0} { set y0 [expr {$y>30? $y-20: $y+25}] set x0 [expr {$y>30? $newX1: $newX}] $c create line $x0 $y0 $x0 [incr y 3] -tag note if {[string first - $length]>=0} { set y1 [expr {($y0+$y)/2}] $c create line $x0 $y0 [expr {$x0+5}] $y1 \ -width 1 -tag note } } } proc music::makeNoteTable {y0 dy} { set basics {C D E F G A B} foreach i "$basics [string tolower $basics]" { lappend noteTable $i $y0 incr y0 -$dy } set noteTable } #-------------------------------------------- End of package contents package provide music $music::version#----------------------------------------------- Tk and pure-Tcl demos
if {[file tail [info script]]==[file tail $argv0]} { set tune { e. d c c. A- A. G+ c e d+ / e. d c c. A- A. G c B d c+ x > g. a g g. e- g. g+ a g d+ < / e. d c c. A- A. G c B d c++ } if {[package provide Tk]!=""} { option add *Button.padY 0 wm title . "Tclmusic $music::version demo" canvas .s -bg white -height 80 music::drawLines .s 0 20 1000 6 frame .f button .f.play -text Play -command {music::play $tune 1} button .f.x -text X -command {set tune ""} checkbutton .f.record -text Record -variable music::record checkbutton .f.notes -text Notes -variable music::showNotes eval pack [winfo children .f] -side left -pady 0 -fill y entry .e -textvar tune bind .e <Return> {.f.play invoke} bind .e <3> {catch {music::play [selection get] 1}} trace variable music::recorded w {set ::tune $::music::recorded ;#} canvas .c -height 10 ;# dummy small to make it shrinkwrapped music::drawKeyboard .c 5 5 16 100 61 label .info -textvar info -width 80 -anchor w -relief sunken \ -borderwidth 1 set info "Welcome to TclMusic - enjoy the power of Tcl/Tk/Snack!" trace variable music::current w {set ::info $::music::current ;#} eval pack [winfo children .] -fill x wm resizable . 0 0 bind . <Escape> {exec wish $argv0 &; exit} bind . ? {console show} } else { puts "Pure-Tcl music package demo - will last 50 seconds" after 50000 set awhile 1 trace variable music::current w { puts -nonewline stderr "$::music::current " ;#} music::play $tune vwait awhile } }
Kroc - ready to use starkit available at http://www.zolli.fr/fichiers/TclMusic.zip
[ Category Package | Arts and crafts of Tcl-Tk programming | Category Toys | Category Sound | Category Music ]