# -*- coding: utf-8 -*-
#! /usr/bin/python
#
# Copyright (C) 2009-12 Walter Bender
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# You should have received a copy of the GNU General Public License
# along with this library; if not, write to the Free Software
# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
import os
import gtk
import gobject
from gettext import gettext as _
from config import XO4, XO175, INSTRUMENT_DICT
from audiograb import check_output
from sugar.graphics.toolbutton import ToolButton
from sugar.graphics.menuitem import MenuItem
from sugar.graphics import style
import logging
log = logging.getLogger('measure-activity')
log.setLevel(logging.DEBUG)
NOTES = ['A', 'A♯/B♭', 'B', 'C', 'C♯/D♭', 'D', 'D♯/E♭', 'E', 'F', 'F♯/G♭',
'G', 'G♯/A♭']
SHARP = '♯'
FLAT = '♭'
A0 = 27.5
C8 = 4186.01
TWELTHROOT2 = 1.05946309435929
COLOR_RED = style.Color('#FF6060')
COLOR_YELLOW = style.Color('#FFFF00')
COLOR_GREEN = style.Color('#00FF00')
SPAN = '%s'
class TuningToolbar(gtk.Toolbar):
''' The toolbar for tuning instruments '''
def __init__(self, activity):
gtk.Toolbar.__init__(self)
self.activity = activity
self._show_tuning_line = False
self._updating_note = True
self._tuning_tool = None
self._instrument_button = ToolButton('instruments')
self._instrument_button.set_tooltip(_('Tune an instrument.'))
self._instrument_button.connect('clicked',
self._button_selection_cb)
self.insert(self._instrument_button, -1)
self._setup_instrument_palette()
separator = gtk.SeparatorToolItem()
separator.props.draw = True
self.insert(separator, -1)
self._note = 'A'
self._notes_button = ToolButton('notes')
self._notes_button.set_tooltip(_('Notes'))
self._notes_button.connect('clicked',
self._button_selection_cb)
self.insert(self._notes_button, -1)
self._setup_notes_palette()
self._octave = 4
self._octaves_button = ToolButton('octaves')
self._octaves_button.set_tooltip(_('Octaves'))
self._octaves_button.connect('clicked',
self._button_selection_cb)
self.insert(self._octaves_button, -1)
self._setup_octaves_palette()
# The entry is used to display a note or for direct user input
self._freq_entry = gtk.Entry()
self._freq_entry.set_text('440') # A
self._freq_entry_changed_id = self._freq_entry.connect(
'changed', self._update_freq_entry)
if hasattr(self._freq_entry, 'set_tooltip_text'):
self._freq_entry.set_tooltip_text(
_('Enter a frequency to display.'))
self._freq_entry.set_width_chars(8)
self._freq_entry.show()
toolitem = gtk.ToolItem()
toolitem.add(self._freq_entry)
self.insert(toolitem, -1)
toolitem.show()
self._new_tuning_line = ToolButton('tuning-tools')
self._new_tuning_line.show()
self.insert(self._new_tuning_line, -1)
self._new_tuning_line.set_tooltip(_('Show tuning line.'))
self._new_tuning_line.connect('clicked', self.tuning_line_cb)
separator = gtk.SeparatorToolItem()
separator.props.draw = True
self.insert(separator, -1)
self._harmonic = ToolButton('harmonics')
self._harmonic.show()
self.insert(self._harmonic, -1)
self._harmonic.set_tooltip(_('Show harmonics.'))
self._harmonic.connect('clicked', self.harmonic_cb)
separator = gtk.SeparatorToolItem()
separator.props.draw = True
self.insert(separator, -1)
self._play_tone = ToolButton('media-playback-start')
self._play_tone.show()
self.insert(self._play_tone, -1)
self._play_tone.set_tooltip(_('Play a note.'))
self._play_tone.connect('clicked', self.play_cb)
separator = gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_expand(True)
self.insert(separator, -1)
self.label = gtk.Label('')
self.label.set_use_markup(True)
self.label.show()
toolitem = gtk.ToolItem()
toolitem.add(self.label)
self.insert(toolitem, -1)
toolitem.show()
self.show_all()
def _update_note(self):
''' Calculate the frequency based on note and octave '''
if not hasattr(self, '_freq_entry'): # Still setting up toolbar
return
i = self._octave * 12 + NOTES.index(self._note)
freq = A0 * pow(TWELTHROOT2, i)
self._updating_note = True
self._freq_entry.set_text('%0.3f' % (freq))
self.label.set_markup(SPAN % (style.COLOR_WHITE.get_html(),
self._note + str(self._octave)))
if self._show_tuning_line:
self.activity.wave.tuning_line = freq
return
def _update_freq_entry(self, widget):
# Calculate a note from a frequency
if not self._updating_note: # Only if user types in a freq.
try:
freq = float(self._freq_entry.get_text())
# Only consider notes in piano range
if freq < A0 * 0.97:
self.label.set_text('< A0')
return
if freq > C8 * 1.03:
self.label.set_text('> C8')
return
self.label.set_markup(freq_note(freq, flatsharp=True))
except ValueError:
return
self._updating_note = False
def _button_selection_cb(self, widget):
palette = widget.get_palette()
if palette:
if not palette.is_up():
palette.popup(immediate=True, state=palette.SECONDARY)
else:
palette.popdown(immediate=True)
return
def _setup_notes_palette(self):
self._notes_palette = self._notes_button.get_palette()
for note in NOTES:
menu_item = MenuItem(icon_name='',
text_label=note)
menu_item.connect('activate', self._note_selected_cb, note)
self._notes_palette.menu.append(menu_item)
menu_item.show()
def _note_selected_cb(self, widget, note):
self._note = note
self._update_note()
def _setup_octaves_palette(self):
self._octaves_palette = self._octaves_button.get_palette()
for octave in range(9):
menu_item = MenuItem(icon_name='',
text_label=str(octave))
menu_item.connect('activate', self._octave_selected_cb, octave)
self._octaves_palette.menu.append(menu_item)
menu_item.show()
def _octave_selected_cb(self, widget, octave):
self._octave = octave
self._update_note()
def _setup_instrument_palette(self):
self.instrument_palette = self._instrument_button.get_palette()
self.instrument = []
for k in INSTRUMENT_DICT.keys():
self.instrument.append(k)
menu_item = MenuItem(icon_name='',
text_label=k)
menu_item.connect('activate', self.instrument_selected_cb, k)
self.instrument_palette.menu.append(menu_item)
menu_item.show()
def instrument_selected_cb(self, button, instrument):
''' Callback for instrument control '''
logging.debug(instrument)
if self._tuning_tool is not None:
self.remove(self._tuning_tool)
if instrument == _('None'):
self.activity.wave.instrument = None
# Remove any previous tuning button
if hasattr(self, '_tuning_button'):
self._tuning_button.destroy()
# Restore the notes, octaves buttons
if hasattr(self, '_notes_button'):
self.insert(self._notes_button, 2)
self.insert(self._octaves_button, 3)
return
self.remove(self._notes_button)
self.remove(self._octaves_button)
self.activity.wave.instrument = instrument
# If we are not already in freq. base, switch.
if not self.activity.wave.get_fft_mode():
self.activity.timefreq_control()
# Add a Tuning palette for this instrument
self._tuning_button = ToolButton('notes')
self._tuning_button.set_tooltip(instrument)
self._tuning_button.connect('clicked', self._button_selection_cb)
self.insert(self._tuning_button, 1)
self._setup_tuning_palette(instrument)
def _setup_tuning_palette(self, instrument):
self._tuning_palette = self._tuning_button.get_palette()
self.tuning = []
self.tuning.append(_('All notes'))
menu_item = MenuItem(icon_name='', text_label=_('All notes'))
menu_item.connect('activate', self._tuning_selected_cb,
instrument, -1)
self._tuning_palette.menu.append(menu_item)
menu_item.show()
for i, f in enumerate(INSTRUMENT_DICT[instrument]):
self.tuning.append(freq_note(f))
menu_item = MenuItem(icon_name='',
text_label=freq_note(f))
menu_item.connect('activate', self._tuning_selected_cb,
instrument, i)
self._tuning_palette.menu.append(menu_item)
menu_item.show()
self.show_all()
def _tuning_selected_cb(self, widget, instrument, fidx):
''' Update note '''
if not hasattr(self, '_freq_entry'): # Still setting up toolbar?
return
if not instrument in INSTRUMENT_DICT:
return
if fidx == -1: # All notes
self.activity.wave.instrument = instrument
self.activity.wave.tuning_line = 0.0
self._new_tuning_line.set_icon('tuning-tools')
self._new_tuning_line.set_tooltip(_('Show tuning line.'))
self._show_tuning_line = False
else:
freq = INSTRUMENT_DICT[instrument][fidx]
self.activity.wave.instrument = None
self.activity.wave.tuning_line = freq
self._new_tuning_line.set_icon('tuning-tools-off')
self._new_tuning_line.set_tooltip(_('Hide tuning line.'))
self._show_tuning_line = True
self._updating_note = False
def harmonic_cb(self, *args):
''' Callback for harmonics control '''
self.activity.wave.harmonics = not self.activity.wave.harmonics
if self.activity.wave.harmonics:
self._harmonic.set_icon('harmonics-off')
self._harmonic.set_tooltip(_('Hide harmonics.'))
if self.activity.wave.instrument is None and \
self.activity.wave.tuning_line == 0.0:
self._load_tuning_line()
else:
self._harmonic.set_icon('harmonics')
self._harmonic.set_tooltip(_('Show harmonics.'))
def tuning_line_cb(self, *args):
''' Callback for tuning insert '''
if self._show_tuning_line:
self.activity.wave.tuning_line = 0.0
self._new_tuning_line.set_icon('tuning-tools')
self._new_tuning_line.set_tooltip(_('Show tuning line.'))
self._show_tuning_line = False
else:
self._load_tuning_line()
def _load_tuning_line(self):
''' Read the freq entry and use value to set tuning line '''
freq = self._freq_entry.get_text()
try:
self.activity.wave.tuning_line = float(freq)
if freq < 0:
freq = -freq
self._new_tuning_line.set_icon('tuning-tools-off')
self._new_tuning_line.set_tooltip(_('Hide tuning line.'))
self._show_tuning_line = True
except ValueError:
self.activity.wave.tuning_line = 0.0
self._freq_entry.set_text('0')
# If we are not already in freq. base, switch.
if not self.activity.wave.get_fft_mode():
self.activity.timefreq_control()
def play_cb(self, *args):
''' Save settings, turn off display, and then play a tone at
the current frequency '''
channels = []
for c in range(self.activity.audiograb.channels):
channels.append(self.activity.wave.get_visibility(channel=c))
self.activity.wave.set_visibility(False, channel=c)
wave_status = self.activity.wave.get_active()
self.activity.wave.set_context_off()
self.activity.wave.set_active(False)
if self.activity.hw in [XO4, XO175]:
self.activity.audiograb.stop_grabbing()
freq = float(self._freq_entry.get_text())
gobject.timeout_add(200, self.play_sound, freq, channels, wave_status)
def play_sound(self, freq, channels, wave_status):
''' Play the sound and then restore wave settings '''
self._play_sinewave(freq, 5000, 1)
if self.activity.hw in [XO4, XO175]:
self.activity.sensor_toolbar.set_mode('sound')
self.activity.sensor_toolbar.set_sound_context()
self.activity.audiograb.start_grabbing()
for c in range(self.activity.audiograb.channels):
self.activity.wave.set_visibility(channels[c], channel=c)
self.activity.wave.set_context_on()
self.activity.wave.set_active(wave_status)
def _play_sinewave(self, pitch, amplitude=5000, duration=1):
""" Create a Csound score to play a sine wave. """
self.orchlines = []
self.scorelines = []
self.instrlist = []
try:
pitch = abs(float(pitch))
amplitude = abs(float(amplitude))
duration = abs(float(duration))
except ValueError:
logging.error('bad args to _play_sinewave')
return
self._prepare_sinewave(pitch, amplitude, duration)
path = os.path.join(self.activity.get_activity_root(), 'instance',
'tmp.csd')
# Create a csound file from the score.
self._audio_write(path)
# Play the csound file.
output = check_output(['csound', path], 'call to csound failed?')
# os.system('csound ' + path + ' > /dev/null 2>&1')
def _prepare_sinewave(self, pitch, amplitude, duration, starttime=0,
pitch_envelope=99, amplitude_envelope=100,
instrument=1):
pitenv = pitch_envelope
ampenv = amplitude_envelope
if not 1 in self.instrlist:
self.orchlines.append("instr 1\n")
self.orchlines.append("kpitenv oscil 1, 1/p3, p6\n")
self.orchlines.append("aenv oscil 1, 1/p3, p7\n")
self.orchlines.append("asig oscil p5*aenv, p4*kpitenv, p8\n")
self.orchlines.append("out asig\n")
self.orchlines.append("endin\n\n")
self.instrlist.append(1)
self.scorelines.append("i1 %s %s %s %s %s %s %s\n" %
(str(starttime), str(duration), str(pitch),
str(amplitude), str(pitenv), str(ampenv),
str(instrument)))
def _audio_write(self, file):
""" Compile a .csd file. """
csd = open(file, "w")
csd.write("\n\n")
csd.write("\n")
csd.write("-+rtaudio=alsa -odevaudio -m0 -d -b256 -B512\n")
csd.write("\n\n")
csd.write("\n\n")
csd.write("sr=16000\n")
csd.write("ksmps=50\n")
csd.write("nchnls=1\n\n")
for line in self.orchlines:
csd.write(line)
csd.write("\n\n\n")
csd.write("\n\n")
csd.write("f1 0 2048 10 1\n")
csd.write("f2 0 2048 10 1 0 .33 0 .2 0 .143 0 .111\n")
csd.write("f3 0 2048 10 1 .5 .33 .25 .2 .175 .143 .125 .111 .1\n")
csd.write("f10 0 2048 10 1 0 0 .3 0 .2 0 0 .1\n")
csd.write("f99 0 2048 7 1 2048 1\n")
csd.write("f100 0 2048 7 0. 10 1. 1900 1. 132 0.\n")
csd.write(self.scorelines.pop())
csd.write("e\n")
csd.write("\n\n")
csd.write("\n")
csd.close()
class InstrumentToolbar(gtk.Toolbar):
''' The toolbar for adding new instruments '''
def __init__(self, activity):
gtk.Toolbar.__init__(self)
self.activity = activity
self.new_instruments = []
self._name_entry = gtk.Entry()
self._name_entry.set_text(_('my instrument'))
self._name_entry_changed_id = self._name_entry.connect(
'changed', self.update_name_entry)
if hasattr(self._name_entry, 'set_tooltip_text'):
self._name_entry.set_tooltip_text(
_('Enter instrument name.'))
self._name_entry.set_width_chars(24)
self._name_entry.show()
toolitem = gtk.ToolItem()
toolitem.add(self._name_entry)
self.insert(toolitem, -1)
toolitem.show()
self._note = 'A'
self._notes_button = ToolButton('notes')
self._notes_button.set_tooltip(_('Notes'))
self._notes_button.connect('clicked',
self._button_selection_cb)
self.insert(self._notes_button, -1)
self._setup_notes_palette()
self._notes_button.show()
self._octave = 4
self._octaves_button = ToolButton('octaves')
self._octaves_button.set_tooltip(_('Octaves'))
self._octaves_button.connect('clicked',
self._button_selection_cb)
self.insert(self._octaves_button, -1)
self._setup_octaves_palette()
self._octaves_button.show()
self._new_note = ToolButton('list-add')
self._new_note.show()
self.insert(self._new_note, -1)
self._new_note.set_tooltip(_('Add a new note.'))
self._new_note.connect('clicked', self.new_note_cb)
self._new_note.show()
def _button_selection_cb(self, widget):
palette = widget.get_palette()
if palette:
if not palette.is_up():
palette.popup(immediate=True, state=palette.SECONDARY)
else:
palette.popdown(immediate=True)
return
def _setup_notes_palette(self):
self._notes_palette = self._notes_button.get_palette()
for note in NOTES:
menu_item = MenuItem(icon_name='',
text_label=note)
menu_item.connect('activate', self._note_selected_cb, note)
self._notes_palette.menu.append(menu_item)
menu_item.show()
def _note_selected_cb(self, widget, note):
self._note = note
def _setup_octaves_palette(self):
self._octaves_palette = self._octaves_button.get_palette()
for octave in range(9):
menu_item = MenuItem(icon_name='',
text_label=str(octave))
menu_item.connect('activate', self._octave_selected_cb, octave)
self._octaves_palette.menu.append(menu_item)
menu_item.show()
def _octave_selected_cb(self, widget, octave):
self._octave = octave
def update_name_entry(self, *args):
''' Add name to INSTRUMENT_DICT and combo box '''
# Wait until a note has been added...
return
def new_note_cb(self, *args):
''' Add a new note to instrument tuning list '''
name = self._name_entry.get_text()
if name not in INSTRUMENT_DICT:
INSTRUMENT_DICT[name] = []
self.activity.tuning_toolbar.instrument.append(name)
i = len(self.activity.tuning_toolbar.instrument)
menu_item = MenuItem(icon_name='',
text_label=name)
menu_item.connect(
'activate',
self.activity.tuning_toolbar.instrument_selected_cb,
name)
self.activity.tuning_toolbar.instrument_palette.menu.append(
menu_item)
menu_item.show()
self.new_instruments.append(name)
freq = A0 * pow(TWELTHROOT2,
self._octave * 12 + NOTES.index(self._note))
if freq not in INSTRUMENT_DICT[name]:
INSTRUMENT_DICT[name].append(freq)
def note_octave(note, octave):
if '/' in note:
flat, sharp = note.split('/')
return '%s%d/%s%d' % (flat, octave, sharp, octave)
else:
return '%s%d' % (note, octave)
def freq_note(freq, flatsharp=False):
if flatsharp: # calculate if we are sharp or flat
for i in range(88):
f = A0 * pow(TWELTHROOT2, i)
if freq < f * 1.03 and freq > f * 0.97:
label = NOTES[i % 12] + str(int(i / 12))
if freq < f * 0.98:
label = '%s %s %s' % (FLAT, label, FLAT)
return SPAN % (COLOR_RED.get_html(), label)
elif freq < f * 0.99:
label = '%s %s %s' % (FLAT, label, FLAT)
return SPAN % (COLOR_YELLOW.get_html(), label)
elif freq > f * 1.02:
label = '%s %s %s' % (SHARP, label, SHARP)
return SPAN % (COLOR_RED.get_html(), label)
elif freq > f * 1.01:
label = '%s %s %s' % (SHARP, label, SHARP)
return SPAN % (COLOR_YELLOW.get_html(), label)
else:
return SPAN % (style.COLOR_WHITE.get_html(), label)
else:
for i in range(88):
f = A0 * pow(TWELTHROOT2, i)
if freq < f * 1.03 and freq > f * 0.97: # Found a match
return note_octave(NOTES[i % 12], int(i / 12))
return '?'
def freq_index(freq):
for i in range(88):
f = A0 * pow(TWELTHROOT2, i)
if freq < f * 1.03 and freq > f * 0.97: # Found a match
return i
return 0
def index_to_octave(i):
return int((i - 3) / 12) + 1 # -3 because we start with A
def index_to_note(i):
return (i - 3) % 12 # -3 because we start with A