Skip to content

Commit

Permalink
Mac singing-text JSON option for synchronised transcripts; PyPI packa…
Browse files Browse the repository at this point in the history
…ge; minor cleanup
  • Loading branch information
ssb22 committed May 17, 2024
1 parent 57f1a95 commit d639d1c
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*~
16 changes: 16 additions & 0 deletions Makefile.pypi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- mode: Makefile -*-
check-if-midi-beeper-version-is-changing:
if (git diff;git diff --staged)|grep '^[+]# Version [1-9]'; then make -f Makefile.pypi update-midi-beeper-pypi; else true; fi
update-midi-beeper-pypi:
mkdir midi_beeper
echo '"""MIDI Beeper is an application, not a library. You can run it with "python -m midi_beeper" to see the options."""'> midi_beeper/__init__.py
cp midi-beeper.py midi_beeper/__main__.py
echo "def placebo(): pass # for setuptools entry_points" >> midi_beeper/__main__.py # because there's no main()
echo "from setuptools import setup, find_packages;setup(name='midi_beeper',version='$$(grep '^# Version [1-9]' midi-beeper.py|sed -e 's/[^ ]* Version //' -e 's/,.*//')',entry_points={'console_scripts':['midi-beeper=midi_beeper.__main__:placebo']},license='Apache 2',platforms='all',url='http://ssb22.user.srcf.net/mwrhome/midi-beeper.html',author='Silas S. Brown',author_email='ssb$$(echo 22@ca)m.ac.uk',description='Play MIDI files using piezo beepers and other sounders',long_description=r'''$$(grep -v 'also mirrored' < README.md)''',long_description_content_type='text/markdown',packages=find_packages(),classifiers=['Programming Language :: Python :: 2','Programming Language :: Python :: 3','License :: OSI Approved :: Apache Software License','Operating System :: OS Independent'],python_requires='>=2.2')" > setup.py
mv README.md .. # or it'll override our altered version
python3 setup.py sdist
twine upload dist/*
mv ../README.md .
rm -r midi_beeper.egg-info dist midi_beeper setup.py
.PHONY: check-if-midi-beeper-version-is-changing
.PHONY: update-midi-beeper-pypi
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
# midi-beeper
MIDI Beeper from http://ssb22.user.srcf.net/mwrhome/midi-beeper.html
(also mirrored at http://ssb22.gitlab.io/mwrhome/midi-beeper.html just in case)
(also mirrored at http://ssb22.gitlab.io/mwrhome/midi-beeper.html just in case, plus you can access MIDI Beeper via `pip install midi-beeper` or `pipx run midi-beeper`)

MIDI beeper is a program to play MIDI files on Linux/BSD by beeping through the computer’s beeper instead of using proper sound circuits. If you try to play chords or polyphony, it will rapidly switch between alternate notes like an old office telephone. It sounds awful, but it might be useful when you really have to play a MIDI file but have no sound device attached. It should work on any machine that has the “beep” command (install “beep” package from your Linux/Unix package manager). It has been tested on a PC speaker and on an NSLU2’s internal speaker.
MIDI Beeper is a program to play MIDI files on Linux/BSD by beeping through the computer’s beeper instead of using proper sound circuits. If you try to play chords or polyphony, it will rapidly switch between alternate notes like an old office telephone. It sounds awful, but it might be useful when you really have to play a MIDI file but have no sound device attached. It should work on any machine that has the “beep” command (install “beep” package from your Linux/Unix package manager). It has been tested on a PC speaker and on an NSLU2’s internal speaker.

On the NSLU2, playing music with beep works in Debian 4 (Etch, 2007) but not so well in Debian 5 (Lenny, 2012); you can try compiling this [modified beep.c](http://ssb22.user.srcf.net/mwrhome/beep.c) instead (remember the chmod 4755 mentioned in the man page). I haven’t tried it on more recent distros because my NSLU2 power supply failed and I upgraded to a Raspberry Pi.

RISC OS etc
-----------
MIDI Beeper can also generate polyphonic square waves itself and feed them to `aplay`, which might be useful if you need a small MIDI player on a Raspberry Pi running Linux, although too many sound channels can slow this down as it’s only a Python script.

## RISC OS and BBC BASIC

If you need to know what a MIDI file sounds like while using a “vanilla” RISC OS machine, edit midi-beeper and set riscos_Maestro to turn it into a converter from MIDI files to Acorn Maestro files. Rather than rapidly switching between notes, this uses true polyphony of up to 8 channels, although Maestro can struggle with rhythm when playing more than 4 channels. The music may not look good in Maestro (which is not a good program for typesetting anyway), but at least it plays.

Alternatively you can use a BBC Micro emulator (or a real BBC Micro if you still have one from the 1980s) and set MIDI beeper to generate BBC Micro code. This uses 3-channel polyphony and can multiplex up to 9 via envelope arpeggiation (3 on the Electron). The tuning can be a bit ‘wobbly’. Here’s an [example SSD of short compositions](http://ssb22.user.srcf.net/mwrhome/bbcmicro.zip) (~60k for ~33½mins).
Alternatively you can use a BBC Micro emulator (or a real BBC Micro if you still have one from the 1980s) and set MIDI Beeper to generate BBC Micro code. This uses 3-channel polyphony and can multiplex up to 9 via envelope arpeggiation (3 on the Electron). The tuning can be a bit ‘wobbly’. Here’s an [example SSD of short compositions](http://ssb22.user.srcf.net/mwrhome/bbcmicro.zip) (~60k for ~33½mins).

## Other

MIDI Beeper can also generate code for the old `PLAY` command in QBasic for DOS, and for the GNU/Linux GRUB bootloader on machines where this has a beeper available (this is generally no longer the case on modern machines).

MIDI beeper can also generate polyphonic square waves itself and feed them to aplay, which might be useful if you need a small MIDI player on a Raspberry Pi running Linux, although too many sound channels can slow this down as it’s only a Python script.
Options are also provided to render singing text using the Mac voices and the Praat phonetic processor (one channel at a time).

Copyright and Trademarks
------------------------
Expand Down
51 changes: 47 additions & 4 deletions midi-beeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# (can be run in either Python 2 or Python 3)

# MIDI beeper (plays MIDI without sound hardware)
# Version 1.79, (c) 2007-2010,2015-2024 Silas S. Brown
# Version 1.8, (c) 2007-2010,2015-2024 Silas S. Brown
# License: Apache 2 (see below)

# MIDI beeper is a Python program to play MIDI by beeping
Expand Down Expand Up @@ -49,8 +49,14 @@
# No melisma; likely works best with patter songs.
mac_voice = "" # or run with --Organ or --Joelle
# put syllables into environment variable SaySyls
# (comma separated)
# (comma separated; may need to change spelling)
mac_voice_praat_correction = 0 # or run with --praat requires Praat, recommended for Joelle
voice_json = 0 # or run with --json: put environ
# variable SingWords to space-separated words with
# hyphen-separated syllables, preceded by singer
# names in [...], for podcast:transcript on stdout
# tested on Anytime Player and Anemone DAISY Maker
Anytime_Player_bug_workaround = 1

force_monophonic = 0 # set this to 1 to have only the top line (not normally necessary)

Expand Down Expand Up @@ -95,12 +101,13 @@ def delArg(a):
if delArg('--Organ'): mac_voice="Organ"
if delArg('--Joelle'): mac_voice="Joelle"
if delArg('--praat'): mac_voice_praat_correction=1
if delArg('--json'): voice_json=1
assert not (bbc_sdl and (bbc_binary or bbc_ssd)), "bbc_sdl not compatible with bbc_binary or bbc_ssd"

on_riscos = sys.platform.lower().find("riscos")>=0
if on_riscos and not (bbc_micro or acorn_electron): riscos_Maestro = 1
elif not aplay: aplay=int(os.environ.get("APLAY_VOL",0))
if riscos_Maestro or bbc_micro or acorn_electron or grub or qbasic or mac_voice: aplay = 0
if riscos_Maestro or bbc_micro or acorn_electron or grub or qbasic or mac_voice or voice_json: aplay = 0

# To add a new type of beeper, get the following 'if' block to do any necessary global setup and to define the appropriate version of the per-file init() and of add_midi_note_chord(), then check the 'if' after 'ensure flushed' at end, and quantiseTo logic
if aplay:
Expand Down Expand Up @@ -346,6 +353,41 @@ def add_midi_note_chord(noteNos,microsecs):
b=os.popen('sox %d-1.wav -t raw -r 44100 -c 1 -b 16 -' % pid)
pcmData[-1] = (b.buffer if hasattr(b,'buffer') else b).read()
os.remove('%d-1.wav' % pid)
elif voice_json:
force_monophonic = 1
def init():
global SingWords,currentSpeaker,microsecsSoFar,sylsLeft
SingWords = os.environ["SingWords"].split()
SingWords.reverse() # so can use pop()
currentSpeaker = "singer"
microsecsSoFar = int(os.environ.get("SingMicrosecsOffset","0")) # (in case it won't be at the very start of the audio)
sylsLeft = 0
print('{"version":"1.0.0","segments":[')
def setupNextWord():
isSpeaker = 0
while True:
word = SingWords.pop()
global currentSpeaker
if word.startswith('[') or isSpeaker:
if word.startswith('['): currentSpeaker=""
currentSpeaker += word.replace("[","").replace("]","")
isSpeaker = not word.endswith(']')
if isSpeaker: currentSpeaker += " "
else:
global currentWord,sylsLeft ; currentWord,sylsLeft = word.replace('-',''),len(word.split('-'))
if Anytime_Player_bug_workaround: # v1.3.5 drops space before single-letter words
while SingWords and len(SingWords[-1])==1:
currentWord += " "+SingWords.pop()
sylsLeft += 1
break
def add_midi_note_chord(noteNos,microsecs):
global microsecsSoFar, sylsLeft, wordStartMS
startM,microsecsSoFar = microsecsSoFar,microsecsSoFar+microsecs
if not noteNos or not microsecs: return
if not sylsLeft:
wordStartMS = startM ; setupNextWord()
sylsLeft -= 1
if not sylsLeft: print('{"speaker":"%s","startTime":%g,"endTime":%g,"body":"%s"}%s' % (currentSpeaker,wordStartMS/1000000.0,microsecsSoFar/1000000.0,currentWord,(',' if SingWords else ((',{"speaker":"%s","startTime":%g,"endTime":%g,"body":""}' % (currentSpeaker,microsecsSoFar/1000000.0,microsecsSoFar/1000000.0)) if Anytime_Player_bug_workaround else '')))) # (v1.3.5 won't display last word so add a placebo)
elif qbasic:
def init():
global basData, dedup_microsec_quantise
Expand Down Expand Up @@ -870,7 +912,7 @@ def all(x):
else: name = "MIDI Beeper"
sys.stderr.write(name+" (c) 2007-2010, 2015-2024 Silas S. Brown. License: Apache 2\n")
if len(sys.argv)<2:
sys.stderr.write("Syntax: python midi-beeper.py [options] MIDI-filename ...\nOptions: --bbc | --electron | --bbc-binary | --bbc-ssd | --maestro | --grub | --qbasic | --Organ | --Joelle\n")
sys.stderr.write("Syntax: python midi-beeper.py [options] MIDI-filename ...\nOptions: --bbc | --electron | --bbc-binary | --bbc-ssd | --bbc-sdl | --maestro | --grub | --qbasic | --Organ | --Joelle (--praat --json)\n")
sys.exit(1)
try: xrange
except: xrange = range # Python 3
Expand Down Expand Up @@ -907,6 +949,7 @@ def all(x):
wavFile = midiFile.replace(os.extsep+"midi","").replace(os.extsep+"mid","")+os.extsep+"wav"
w=os.popen('sox -t raw -r 44100 -c 1 -b 16 -e signed-integer - '+wavFile,'w')
(w.buffer if hasattr(w,'buffer') else w).write(b''.join(pcmData)) ; w.close() ; sys.stderr.write("Wrote "+wavFile+"\n")
elif voice_json: print("]}")
elif not aplay and not grub:
sys.stderr.write("Playing "+midiFile+"\n")
runBeep(" ".join(cumulative_params))
Expand Down
54 changes: 16 additions & 38 deletions ringtone.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,10 @@
def writeBew(value, length):
return pack('>%s' % {1:'B', 2:'H', 4:'L'}[length], value)
def varLen(value):
if value <= 127:
return 1
elif value <= 16383:
return 2
elif value <= 2097151:
return 3
else:
return 4
if value <= 127: return 1
elif value <= 16383: return 2
elif value <= 2097151: return 3
else: return 4
def writeVar(value):
sevens = to_n_bits(value, varLen(value))
for i in range(len(sevens)-1):
Expand Down Expand Up @@ -76,19 +72,6 @@ def write(self):
self.outfile.write(self.getvalue())
def getvalue(self):
return self.buffer.getvalue()
NOTE_OFF = 0x80
NOTE_ON = 0x90
PATCH_CHANGE = 0xC0
MIDI_CH_PREFIX = 0x20
MIDI_PORT = 0x21
END_OF_TRACK = 0x2F
TEMPO = 0x51
TIMING_CLOCK = 0xF8
ACTIVE_SENSING = 0xFE
SYSTEM_RESET = 0xFF
META_EVENT = 0xFF
def is_status(byte):
return (byte & 0x80) == 0x80
class MidiOutFile:
def update_time(self, new_time=0):
self._relative_time += int(new_time)
Expand All @@ -109,13 +92,13 @@ def event_slice(self, slc):
trk.writeVarLen(self.rel_time())
trk.writeSlice(slc)
def note_on(self, channel=0, note=0x40, velocity=0x40):
slc = fromBytes([NOTE_ON + channel, note, velocity])
slc = fromBytes([0x90 + channel, note, velocity])
self.event_slice(slc)
def note_off(self, channel=0, note=0x40, velocity=0x40):
slc = fromBytes([NOTE_OFF + channel, note, velocity])
slc = fromBytes([0x80 + channel, note, velocity])
self.event_slice(slc)
def patch_change(self, channel, patch):
slc = fromBytes([PATCH_CHANGE + channel, patch])
slc = fromBytes([0xC0 + channel, patch])
self.event_slice(slc)
def header(self, format=0, nTracks=1, division=96):
raw = self.raw_out
Expand All @@ -125,15 +108,6 @@ def header(self, format=0, nTracks=1, division=96):
bew(format, 2)
bew(nTracks, 2)
bew(division, 2)
def eof(self):
self.write()
def meta_slice(self, meta_type, data_slice):
"Writes a meta event"
slc = fromBytes([META_EVENT, meta_type]) + \
writeVar(len(data_slice)) + data_slice
self.event_slice(slc)
def meta_event(self, meta_type, data):
self.meta_slice(meta_type, fromBytes(data))
def start_of_track(self, n_track=0):
self._current_track_buffer = RawOutstreamFile()
self.reset_time()
Expand All @@ -142,13 +116,18 @@ def end_of_track(self):
raw = self.raw_out
raw.writeSlice(B('MTrk'))
track_data = self._current_track_buffer.getvalue()
eot_slice = writeVar(self.rel_time()) + fromBytes([META_EVENT, END_OF_TRACK, 0])
eot_slice = writeVar(self.rel_time()) + fromBytes([0xFF, 0x2F, 0])
raw.writeBew(len(track_data)+len(eot_slice), 4)
raw.writeSlice(track_data)
raw.writeSlice(eot_slice)
def tempo(self, value):
hb, mb, lb = (value>>16 & 0xff), (value>>8 & 0xff), (value & 0xff)
self.meta_slice(TEMPO, fromBytes([hb, mb, lb]))
data_slice = fromBytes([
(value>>16 & 0xff),
(value>>8 & 0xff), (value & 0xff)])
self.event_slice(
fromBytes([0xFF, 0x51]) +
writeVar(len(data_slice)) +
data_slice)

mof = MidiOutFile('ringtone.mid')
mof.header()
Expand All @@ -175,6 +154,5 @@ def tempo(self, value):
mof.update_time(half_burstTime)
mof.update_time(totalCycleLen/10)
mof.update_time(totalCycleLen/2)
mof.end_of_track()
mof.eof()
mof.end_of_track() ; mof.write()
print ("Generated a ringtone.mid (run again for another)")

0 comments on commit d639d1c

Please sign in to comment.